Compare commits
96 commits
Author | SHA1 | Date | |
---|---|---|---|
c3bded9f24 | |||
|
b2c86452df | ||
|
1f6ab39382 | ||
|
c105e54631 | ||
9498496ba8 | |||
906c943ac8 | |||
0442b6b3d6 | |||
d0150def51 | |||
a38fafb451 | |||
262da7ef92 | |||
674e330290 | |||
86bc2f7c2e | |||
|
40ee188762 | ||
23e0df804e | |||
3957651211 | |||
|
529a420c14 | ||
|
2d5e1f8567 | ||
b6b116863c | |||
d89f615fa0 | |||
90e3f0c02b | |||
d6fbe06cab | |||
484e0c8c75 | |||
|
a858fca6da | ||
|
5654e830a9 | ||
|
67bd92cf9b | ||
|
534db45389 | ||
|
6d66243ee1 | ||
|
c6e6b7904f | ||
|
88022b1074 | ||
6cc063f776 | |||
ec47fc130d | |||
87092be0f9 | |||
|
ba26a378e3 | ||
b3f24049ea | |||
3981e575f2 | |||
170214c1ca | |||
e11ffb741f | |||
447c1894d9 | |||
7fa6f2a1b9 | |||
a7ad95eff8 | |||
9e30452744 | |||
bc58d555c9 | |||
24c74a2dee | |||
9de3a6fd76 | |||
16c9657b80 | |||
84cda20738 | |||
66e5384fe9 | |||
f6178c8848 | |||
4639fa3584 | |||
|
8c27aef40c | ||
b6dfaef44b | |||
d2f4d15bb9 | |||
cb3bce833b | |||
b6af459d44 | |||
240833e8d6 | |||
7f731a627f | |||
b752d5f708 | |||
a959736237 | |||
a3468c7781 | |||
a31e921375 | |||
27b5e80a2b | |||
037ade50c5 | |||
072d2c1ae9 | |||
ccf1acd21e | |||
9a274591ac | |||
3e70e402ea | |||
193faeb800 | |||
a4925ee783 | |||
e836d306b8 | |||
fd112cc096 | |||
c1455df969 | |||
2f4d1e2a92 | |||
9dc85c21fe | |||
9218bde745 | |||
41c7006c45 | |||
c139a3d33a | |||
d2b2c951a7 | |||
a322bd9415 | |||
2c4df90d8d | |||
ce945b8391 | |||
ae14a33616 | |||
84dae70b7f | |||
6060f282b4 | |||
9622fa47c3 | |||
92f93861e9 | |||
06850c8b8e | |||
852aa1d6c5 | |||
271bd20707 | |||
2a7fa456e4 | |||
69ecb9334e | |||
2c48722dec | |||
ff18e47036 | |||
25dd6e5ccc | |||
30a417ecf9 | |||
90b886cce5 | |||
914f9a2a25 |
103 changed files with 28549 additions and 13359 deletions
12
.devcontainer/Dockerfile
Normal file
12
.devcontainer/Dockerfile
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
FROM mcr.microsoft.com/devcontainers/javascript-node:0-16-bullseye
|
||||||
|
|
||||||
|
# [Optional] Uncomment this section to install additional OS packages.
|
||||||
|
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
|
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||||
|
|
||||||
|
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
||||||
|
# ARG EXTRA_NODE_VERSION=10
|
||||||
|
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||||
|
|
||||||
|
# [Optional] Uncomment if you want to install more global node modules
|
||||||
|
# RUN su node -c "npm install -g <your-package-list-here>"
|
23
.devcontainer/devcontainer.json
Normal file
23
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// Update the VARIANT arg in docker-compose.yml to pick a Node.js version
|
||||||
|
{
|
||||||
|
"name": "Twigs Web",
|
||||||
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
|
"service": "app",
|
||||||
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/implementors/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// This can be used to network with other containers or with the host.
|
||||||
|
"forwardPorts": [4200, "backend:8080"]
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "yarn install",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
46
.devcontainer/docker-compose.yml
Normal file
46
.devcontainer/docker-compose.yml
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ../..:/workspaces:cached
|
||||||
|
|
||||||
|
# Overrides default command so things don't shut down after the process ends.
|
||||||
|
command: sleep infinity
|
||||||
|
|
||||||
|
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||||
|
network_mode: service:db
|
||||||
|
|
||||||
|
# Uncomment the next line to use a non-root user for all processes.
|
||||||
|
user: node
|
||||||
|
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||||
|
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: ghcr.io/wbrawner/twigs-server:main
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TWIGS_DB_HOST: db
|
||||||
|
TWIGS_DB_NAME: postgres
|
||||||
|
TWIGS_DB_USER: postgres
|
||||||
|
TWIGS_DB_PASS: postgres
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
|
||||||
|
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
|
||||||
|
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
5
.firebaserc
Normal file
5
.firebaserc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"default": "budget-c7da5"
|
||||||
|
}
|
||||||
|
}
|
22
.github/workflows/docker-image.yml
vendored
Normal file
22
.github/workflows/docker-image.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
name: Publish Docker image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: main
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push_to_registry:
|
||||||
|
name: Push Docker image to GitHub Packages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Push to GitHub Packages
|
||||||
|
uses: docker/build-push-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
repository: ${{ github.repository }}/${{ github.event.repository.name }}
|
||||||
|
registry: docker.pkg.github.com
|
||||||
|
tag_with_ref: true
|
20
.github/workflows/firebase-hosting-merge.yml
vendored
Normal file
20
.github/workflows/firebase-hosting-merge.yml
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# This file was auto-generated by the Firebase CLI
|
||||||
|
# https://github.com/firebase/firebase-tools
|
||||||
|
|
||||||
|
name: Deploy to Firebase Hosting on merge
|
||||||
|
'on':
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
build_and_deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- run: npm ci && npm run package
|
||||||
|
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||||
|
with:
|
||||||
|
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_BUDGET_C7DA5 }}'
|
||||||
|
channelId: live
|
||||||
|
projectId: budget-c7da5
|
54
.github/workflows/gh-pages.yml
vendored
Normal file
54
.github/workflows/gh-pages.yml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
name: Deploy to GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||||
|
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Build job
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v4
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 'lts/*'
|
||||||
|
- name: Install dependencies with npm
|
||||||
|
run: npm ci
|
||||||
|
- name: Build with NPM
|
||||||
|
run: npm run package
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: dist/twigs
|
||||||
|
|
||||||
|
# Deployment job
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -25,6 +25,7 @@
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
|
/.angular/cache
|
||||||
/.sass-cache
|
/.sass-cache
|
||||||
/connect.lock
|
/connect.lock
|
||||||
/coverage
|
/coverage
|
||||||
|
@ -42,3 +43,4 @@ Thumbs.db
|
||||||
|
|
||||||
# Firebase
|
# Firebase
|
||||||
.firebase/
|
.firebase/
|
||||||
|
.angular/
|
||||||
|
|
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
|
@ -4,6 +4,13 @@
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "pwa-chrome",
|
||||||
|
"url": "http://localhost:4200",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ng serve",
|
"name": "ng serve",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
|
|
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"database.connections": [
|
|
||||||
{
|
|
||||||
"type": "mysql",
|
|
||||||
"name": "root@captain.intra.wbrawner.com (MySql)",
|
|
||||||
"host": "captain.intra.wbrawner.com:3306",
|
|
||||||
"username": "root",
|
|
||||||
"database": null,
|
|
||||||
"password": "U7YE8YsmES8LHB2B39WXNjTQk4d48LzQEZG3cj6wSb2fgeRLEYtrrqTwiqAhrpR3"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:latest as builder
|
FROM node:lts as builder
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN npm install && \
|
RUN npm install && \
|
||||||
|
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2021 William Brawner
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
50
README.md
50
README.md
|
@ -1,27 +1,49 @@
|
||||||
# Budget
|
# Twigs Web Client
|
||||||
|
|
||||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.1.5.
|
# IMPORTANT: This repository is no longer maintained. The web version of Twigs has been replaced with a server-rendered implementation in the [backend repository](https://github.com/wbrawner/twigs)
|
||||||
|
|
||||||
## Development server
|
Twigs is an open source budgeting app aimed at people who need to share a budget. This project serves as the web front end, and is powered by Angular. The main back end project can be found at [wbrawner/twigs-server](https://github.com/wbrawner/twigs-server)
|
||||||
|
|
||||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
## Building
|
||||||
|
|
||||||
## Code scaffolding
|
You'll need NodeJS and NPM, then run
|
||||||
|
|
||||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
npm run build
|
||||||
|
|
||||||
## Build
|
If you would like to tinker with the site and have it hot reload, then run
|
||||||
|
|
||||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
npm run start
|
||||||
|
|
||||||
## Running unit tests
|
The app will be available at http://localhost:4200
|
||||||
|
|
||||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
## Self-hosting
|
||||||
|
|
||||||
## Running end-to-end tests
|
Eventually the plan is to ship this web app within the JAR for the server, but for now you'll need to run them separately. Before you build the app, be sure to change the `apiUrl` value in [src/environments/environment.prod.ts](src/environments/environment.prod.ts). Then you can run the following command to get an optimized version of the build for production deployments
|
||||||
|
|
||||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
npm run package
|
||||||
|
|
||||||
## Further help
|
This will output the app in a folder called `dist/twigs`, which you can then serve directly with Apache or Nginx or any static file server.
|
||||||
|
|
||||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
## License
|
||||||
|
|
||||||
|
```
|
||||||
|
Copyright (c) 2021 William Brawner
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
```
|
||||||
|
|
44
angular.json
44
angular.json
|
@ -13,7 +13,6 @@
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
"options": {
|
"options": {
|
||||||
"aot": true,
|
|
||||||
"outputPath": "dist/twigs",
|
"outputPath": "dist/twigs",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
|
@ -32,7 +31,13 @@
|
||||||
"src/styles.css",
|
"src/styles.css",
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [],
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimization": false,
|
||||||
|
"namedChunks": true
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
@ -51,9 +56,7 @@
|
||||||
"optimization": true,
|
"optimization": true,
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"extractCss": true,
|
|
||||||
"namedChunks": false,
|
"namedChunks": false,
|
||||||
"aot": true,
|
|
||||||
"extractLicenses": true,
|
"extractLicenses": true,
|
||||||
"vendorChunk": false,
|
"vendorChunk": false,
|
||||||
"buildOptimizer": true,
|
"buildOptimizer": true,
|
||||||
|
@ -73,26 +76,27 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"defaultConfiguration": ""
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "twigs:build"
|
"buildTarget": "twigs:build"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "twigs:build:production"
|
"buildTarget": "twigs:build:production"
|
||||||
},
|
},
|
||||||
"codeserver": {
|
"codeserver": {
|
||||||
"browserTarget": "twigs:build:codeserver"
|
"buildTarget": "twigs:build:codeserver"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "twigs:build"
|
"buildTarget": "twigs:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
|
@ -112,18 +116,6 @@
|
||||||
"src/manifest.json"
|
"src/manifest.json"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@angular-devkit/build-angular:tslint",
|
|
||||||
"options": {
|
|
||||||
"tsConfig": [
|
|
||||||
"src/tsconfig.app.json",
|
|
||||||
"src/tsconfig.spec.json"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"**/node_modules/**"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -142,20 +134,10 @@
|
||||||
"devServerTarget": "twigs:serve:production"
|
"devServerTarget": "twigs:serve:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@angular-devkit/build-angular:tslint",
|
|
||||||
"options": {
|
|
||||||
"tsConfig": "e2e/tsconfig.e2e.json",
|
|
||||||
"exclude": [
|
|
||||||
"**/node_modules/**"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultProject": "twigs",
|
|
||||||
"cli": {
|
"cli": {
|
||||||
"analytics": "b8304464-255e-47bb-976a-7ed81af63238"
|
"analytics": "b8304464-255e-47bb-976a-7ed81af63238"
|
||||||
}
|
}
|
||||||
|
|
16
firebase.json
Normal file
16
firebase.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"hosting": {
|
||||||
|
"public": "dist/twigs",
|
||||||
|
"ignore": [
|
||||||
|
"firebase.json",
|
||||||
|
"**/.*",
|
||||||
|
"**/node_modules/**"
|
||||||
|
],
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "**",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
39123
package-lock.json
generated
39123
package-lock.json
generated
File diff suppressed because it is too large
Load diff
96
package.json
96
package.json
|
@ -2,61 +2,57 @@
|
||||||
"name": "budget",
|
"name": "budget",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "node_modules/@angular/cli/bin/ng",
|
"ng": "ng",
|
||||||
"start": "node_modules/@angular/cli/bin/ng serve --host '0.0.0.0'",
|
"start": "ng serve --configuration=production --host '0.0.0.0'",
|
||||||
"code-server": "node_modules/@angular/cli/bin/ng serve --configuration=codeserver --host \"0.0.0.0\" --disable-host-check --poll=2000",
|
"code-server": "ng serve --configuration=codeserver --host \"0.0.0.0\" --disable-host-check --poll=2000",
|
||||||
"build": "node_modules/@angular/cli/bin/ng build",
|
"build": "ng build",
|
||||||
"package": "node_modules/@angular/cli/bin/ng build --prod --service-worker",
|
"package": "ng build --configuration=production --service-worker",
|
||||||
"publish": "node_modules/@angular/cli/bin/ng build --prod --service-worker && firebase deploy",
|
"publish": "ng build --configuration=production --service-worker && firebase deploy",
|
||||||
"test": "node_modules/@angular/cli/bin/ng test",
|
"test": "ng test",
|
||||||
"lint": "node_modules/@angular/cli/bin/ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "node_modules/@angular/cli/bin/ng e2e"
|
"e2e": "ng e2e",
|
||||||
|
"update": "ncu -u"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^10.0.1",
|
"@angular/animations": "^17.2.3",
|
||||||
"@angular/cdk": "^10.0.1",
|
"@angular/cdk": "^17.2.1",
|
||||||
"@angular/common": "^10.0.1",
|
"@angular/common": "^17.2.3",
|
||||||
"@angular/compiler": "^10.0.1",
|
"@angular/compiler": "^17.2.3",
|
||||||
"@angular/core": "^10.0.1",
|
"@angular/core": "^17.2.3",
|
||||||
"@angular/forms": "^10.0.1",
|
"@angular/forms": "^17.2.3",
|
||||||
"@angular/material": "^10.0.1",
|
"@angular/material": "^16.2.0",
|
||||||
"@angular/platform-browser": "^10.0.1",
|
"@angular/platform-browser": "^17.2.3",
|
||||||
"@angular/platform-browser-dynamic": "^10.0.1",
|
"@angular/platform-browser-dynamic": "^17.2.3",
|
||||||
"@angular/router": "^10.0.1",
|
"@angular/router": "^17.2.3",
|
||||||
"@angular/service-worker": "^10.0.1",
|
"@angular/service-worker": "^17.2.3",
|
||||||
"chart.js": "^2.9.3",
|
"chart.js": "^3.7.0",
|
||||||
"core-js": "^2.6.11",
|
"core-js": "^3.20.3",
|
||||||
"dexie": "^2.0.4",
|
"ng2-charts": "^3.0.8",
|
||||||
"firebase": "^7.15.5",
|
"rxjs": "^7.5.2",
|
||||||
"ng2-charts": "^2.3.2",
|
"tslib": "^2.3.1",
|
||||||
"ng2-currency-mask": "^9.0.2",
|
"zone.js": "~0.14.4"
|
||||||
"ngx-cookie-service": "^2.4.0",
|
|
||||||
"rxjs": "^6.5.4",
|
|
||||||
"tslib": "^2.0.0",
|
|
||||||
"zone.js": "~0.10.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.1000.0",
|
"@angular-devkit/build-angular": "^17.2.2",
|
||||||
"@angular/cli": "^10.0.0",
|
"@angular/cli": "^17.2.2",
|
||||||
"@angular/compiler-cli": "^10.0.1",
|
"@angular/compiler-cli": "^17.2.3",
|
||||||
"@angular/language-service": "^10.0.1",
|
"@angular/language-service": "^17.2.3",
|
||||||
"@types/jasmine": "^3.5.2",
|
"@types/jasmine": "~3.10.3",
|
||||||
"@types/jasminewd2": "^2.0.8",
|
"@types/jasminewd2": "^2.0.10",
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^17.0.10",
|
||||||
"codelyzer": "^5.1.2",
|
"eslint": "^8.7.0",
|
||||||
"eslint": "^6.8.0",
|
"jasmine-core": "~4.0.0",
|
||||||
"firebase-tools": "^7.12.1",
|
"jasmine-spec-reporter": "~7.0.0",
|
||||||
"jasmine-core": "~3.5.0",
|
"karma": "~6.3.11",
|
||||||
"jasmine-spec-reporter": "~5.0.0",
|
|
||||||
"karma": "~5.0.0",
|
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
"karma-coverage-istanbul-reporter": "~3.0.3",
|
||||||
"karma-jasmine": "~3.3.0",
|
"karma-jasmine": "~4.0.1",
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.7.0",
|
||||||
"protractor": "~7.0.0",
|
"npm-check-updates": "^15.0.1",
|
||||||
"ts-node": "~8.6.2",
|
"protractor": "^7.0.0",
|
||||||
"tslint": "~6.1.0",
|
"ts-node": "~10.4.0",
|
||||||
"typescript": "3.9.5"
|
"tslint": "^6.1.3",
|
||||||
|
"typescript": "5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,6 +10,7 @@ import { LoginComponent } from './users/login/login.component';
|
||||||
import { RegisterComponent } from './users/register/register.component';
|
import { RegisterComponent } from './users/register/register.component';
|
||||||
import { BudgetsComponent } from './budgets/budget.component';
|
import { BudgetsComponent } from './budgets/budget.component';
|
||||||
import { NewBudgetComponent } from './budgets/new-budget/new-budget.component';
|
import { NewBudgetComponent } from './budgets/new-budget/new-budget.component';
|
||||||
|
import { EditBudgetComponent } from './budgets/edit-budget/edit-budget.component';
|
||||||
import { BudgetDetailsComponent } from './budgets/budget-details/budget-details.component';
|
import { BudgetDetailsComponent } from './budgets/budget-details/budget-details.component';
|
||||||
import { EditCategoryComponent } from './categories/edit-category/edit-category.component';
|
import { EditCategoryComponent } from './categories/edit-category/edit-category.component';
|
||||||
|
|
||||||
|
@ -20,18 +21,19 @@ const routes: Routes = [
|
||||||
{ path: 'budgets', component: BudgetsComponent },
|
{ path: 'budgets', component: BudgetsComponent },
|
||||||
{ path: 'budgets/new', component: NewBudgetComponent },
|
{ path: 'budgets/new', component: NewBudgetComponent },
|
||||||
{ path: 'budgets/:id', component: BudgetDetailsComponent },
|
{ path: 'budgets/:id', component: BudgetDetailsComponent },
|
||||||
{ path: 'budgets/:budgetId/transactions', component: TransactionsComponent },
|
{ path: 'budgets/:id/edit', component: EditBudgetComponent },
|
||||||
{ path: 'budgets/:budgetId/transactions/new', component: NewTransactionComponent },
|
{ path: 'transactions', component: TransactionsComponent },
|
||||||
{ path: 'budgets/:budgetId/transactions/:id', component: TransactionDetailsComponent },
|
{ path: 'transactions/new', component: NewTransactionComponent },
|
||||||
{ path: 'budgets/:budgetId/categories', component: CategoriesComponent },
|
{ path: 'transactions/:id', component: TransactionDetailsComponent },
|
||||||
{ path: 'budgets/:budgetId/categories/new', component: NewCategoryComponent },
|
{ path: 'categories', component: CategoriesComponent },
|
||||||
{ path: 'budgets/:budgetId/categories/:id', component: CategoryDetailsComponent },
|
{ path: 'categories/new', component: NewCategoryComponent },
|
||||||
{ path: 'budgets/:budgetId/categories/:id/edit', component: EditCategoryComponent },
|
{ path: 'categories/:id', component: CategoryDetailsComponent },
|
||||||
|
{ path: 'categories/:id/edit', component: EditCategoryComponent },
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot(routes)
|
RouterModule.forRoot(routes, {})
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
RouterModule
|
RouterModule
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
mat-toolbar {
|
mat-toolbar {
|
||||||
|
background-color: #fafafa;
|
||||||
|
box-shadow: none;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
mat-toolbar {
|
||||||
background-color: #303030;
|
background-color: #303030;
|
||||||
box-shadow: none;
|
|
||||||
padding-left: 0.5em;
|
|
||||||
padding-right: 0.5em;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 999999;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,19 +3,21 @@
|
||||||
</p>
|
</p>
|
||||||
<mat-sidenav-container *ngIf="online" class="sidenav-container">
|
<mat-sidenav-container *ngIf="online" class="sidenav-container">
|
||||||
<mat-sidenav #sidenav mode="over" closed>
|
<mat-sidenav #sidenav mode="over" closed>
|
||||||
<mat-nav-list (click)="sidenav.close()">
|
<mat-nav-list (click)="sidenav.close()" *ngIf="loggedIn">
|
||||||
<a mat-list-item *ngIf="loggedIn" routerLink="">{{ getUsername() }}</a>
|
<a mat-list-item routerLink="">{{ getUsername() }}</a>
|
||||||
<a mat-list-item *ngIf="loggedIn" routerLink="/budgets">Budgets</a>
|
<a mat-list-item routerLink="/budgets">Budgets</a>
|
||||||
<a mat-list-item *ngIf="!loggedIn" routerLink="/login">Login</a>
|
<a mat-list-item (click)="logout()">Logout</a>
|
||||||
<a mat-list-item *ngIf="!loggedIn" routerLink="/register">Register</a>
|
</mat-nav-list>
|
||||||
<a mat-list-item *ngIf="loggedIn" (click)="logout()">Logout</a>
|
<mat-nav-list (click)="sidenav.close()" *ngIf="!loggedIn">
|
||||||
|
<a mat-list-item routerLink="/login">Login</a>
|
||||||
|
<a mat-list-item routerLink="/register">Register</a>
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
<mat-sidenav-content>
|
<mat-sidenav-content>
|
||||||
<mat-toolbar>
|
<mat-toolbar>
|
||||||
<span>
|
<span>
|
||||||
<a mat-icon-button *ngIf="backEnabled" (click)="goBack()">
|
<a mat-icon-button *ngIf="backEnabled" (click)="goBack()">
|
||||||
<mat-icon>arrow_back</mat-icon>
|
<mat-icon>arrow_back</mat-icon>
|
||||||
</a>
|
</a>
|
||||||
<a mat-icon-button *ngIf="!backEnabled" (click)="sidenav.open()">
|
<a mat-icon-button *ngIf="!backEnabled" (click)="sidenav.open()">
|
||||||
<mat-icon>menu</mat-icon>
|
<mat-icon>menu</mat-icon>
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import { TestBed, async } from '@angular/core/testing';
|
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent
|
AppComponent
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
it('should create the app', async(() => {
|
it('should create the app', waitForAsync(() => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.debugElement.componentInstance;
|
const app = fixture.debugElement.componentInstance;
|
||||||
expect(app).toBeTruthy();
|
expect(app).toBeTruthy();
|
||||||
}));
|
}));
|
||||||
it(`should have as title 'budget'`, async(() => {
|
it(`should have as title 'budget'`, waitForAsync(() => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.debugElement.componentInstance;
|
const app = fixture.debugElement.componentInstance;
|
||||||
expect(app.title).toEqual('budget');
|
expect(app.title).toEqual('budget');
|
||||||
}));
|
}));
|
||||||
it('should render title in a h1 tag', async(() => {
|
it('should render title in a h1 tag', waitForAsync(() => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const compiled = fixture.debugElement.nativeElement;
|
const compiled = fixture.debugElement.nativeElement;
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { Component, Inject, ApplicationRef } from '@angular/core';
|
import { Component, Inject, ApplicationRef, ChangeDetectorRef, OnInit } from '@angular/core';
|
||||||
import { Location } from '@angular/common';
|
import { DOCUMENT, Location } from '@angular/common';
|
||||||
import { User } from './users/user';
|
import { User } from './users/user';
|
||||||
import { TWIGS_SERVICE, TwigsService } from './shared/twigs.service';
|
import { TWIGS_SERVICE, TwigsService } from './shared/twigs.service';
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
|
||||||
import { SwUpdate } from '@angular/service-worker';
|
import { SwUpdate } from '@angular/service-worker';
|
||||||
import { first, filter, map } from 'rxjs/operators';
|
import { first, filter, map } from 'rxjs/operators';
|
||||||
import { interval, concat, BehaviorSubject } from 'rxjs';
|
import { interval, concat, BehaviorSubject } from 'rxjs';
|
||||||
|
@ -10,76 +9,126 @@ import { Router, ActivationEnd, ActivatedRoute } from '@angular/router';
|
||||||
import { Actionable, isActionable } from './shared/actionable';
|
import { Actionable, isActionable } from './shared/actionable';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.css']
|
styleUrls: ['./app.component.css']
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent implements OnInit {
|
||||||
public title = 'Twigs';
|
public title = 'Twigs';
|
||||||
public backEnabled = false;
|
public backEnabled = false;
|
||||||
public user = new BehaviorSubject<User>(null);
|
public user = new BehaviorSubject<User>(null);
|
||||||
public online = window.navigator.onLine;
|
public online = window.navigator.onLine;
|
||||||
public currentVersion = '';
|
public currentVersion = '';
|
||||||
public actionable: Actionable;
|
public actionable: Actionable;
|
||||||
public loggedIn = false;
|
public loggedIn = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
private cookieService: CookieService,
|
private router: Router,
|
||||||
private router: Router,
|
private appRef: ApplicationRef,
|
||||||
private activatedRoute: ActivatedRoute,
|
private updates: SwUpdate,
|
||||||
private appRef: ApplicationRef,
|
private changeDetector: ChangeDetectorRef,
|
||||||
private updates: SwUpdate,
|
private storage: Storage,
|
||||||
) {
|
@Inject(DOCUMENT) private document: Document
|
||||||
if (this.cookieService.check('Authorization')) {
|
) { }
|
||||||
this.twigsService.getProfile().subscribe(user => {
|
|
||||||
this.user.next(user);
|
ngOnInit(): void {
|
||||||
if (this.activatedRoute.pathFromRoot.length == 0) {
|
const unauthenticatedRoutes = [
|
||||||
this.router.navigateByUrl("/budgets")
|
'',
|
||||||
|
'/',
|
||||||
|
'/login',
|
||||||
|
'/register'
|
||||||
|
]
|
||||||
|
let auth = this.storage.getItem('Authorization');
|
||||||
|
let userId = this.storage.getItem('userId');
|
||||||
|
let savedUser = JSON.parse(this.storage.getItem('user')) as User;
|
||||||
|
if (auth && auth.length == 255 && userId) {
|
||||||
|
if (savedUser) {
|
||||||
|
this.user.next(savedUser);
|
||||||
|
}
|
||||||
|
this.twigsService.getProfile(userId).then(fetchedUser => {
|
||||||
|
this.storage.setItem('user', JSON.stringify(fetchedUser));
|
||||||
|
this.user.next(fetchedUser);
|
||||||
|
if (unauthenticatedRoutes.indexOf(this.location.path()) != -1) {
|
||||||
|
//TODO: Save last opened budget and redirect to there instead of the main list
|
||||||
|
this.router.navigateByUrl("/budgets");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (unauthenticatedRoutes.indexOf(this.location.path()) == -1) {
|
||||||
|
this.router.navigateByUrl(`/login?redirect=${this.location.path()}`);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} else {
|
this.updates.versionUpdates.subscribe(
|
||||||
this.router.navigateByUrl("/login")
|
event => {
|
||||||
|
if (event.type == "VERSION_READY") {
|
||||||
|
console.log('current version is', event.currentVersion);
|
||||||
|
console.log('available version is', event.latestVersion);
|
||||||
|
// TODO: Prompt user to click something to update
|
||||||
|
this.updates.activateUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true));
|
||||||
|
const everySixHours$ = interval(6 * 60 * 60 * 1000);
|
||||||
|
const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
|
||||||
|
everySixHoursOnceAppIsStable$.subscribe(() => this.updates.checkForUpdate());
|
||||||
|
this.user.subscribe(
|
||||||
|
user => {
|
||||||
|
if (user) {
|
||||||
|
this.loggedIn = true;
|
||||||
|
} else {
|
||||||
|
this.loggedIn = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const darkMode = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
this.handleDarkModeChanges(darkMode);
|
||||||
|
darkMode.addEventListener('change', (e => this.handleDarkModeChanges(e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
updates.available.subscribe(event => {
|
getUsername(): String {
|
||||||
console.log('current version is', event.current);
|
return this.user.value.username;
|
||||||
console.log('available version is', event.available);
|
}
|
||||||
// TODO: Prompt user to click something to update
|
|
||||||
updates.activateUpdate();
|
|
||||||
});
|
|
||||||
updates.activated.subscribe(event => {
|
|
||||||
console.log('old version was', event.previous);
|
|
||||||
console.log('new version is', event.current);
|
|
||||||
});
|
|
||||||
|
|
||||||
const appIsStable$ = appRef.isStable.pipe(first(isStable => isStable === true));
|
goBack(): void {
|
||||||
const everySixHours$ = interval(6 * 60 * 60 * 1000);
|
this.location.back();
|
||||||
const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
|
}
|
||||||
everySixHoursOnceAppIsStable$.subscribe(() => updates.checkForUpdate());
|
|
||||||
this.user.subscribe(
|
logout(): void {
|
||||||
user => {
|
this.twigsService.logout().then(_ => {
|
||||||
if (user) {
|
this.location.go('/');
|
||||||
this.loggedIn = true;
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setActionable(actionable: Actionable): void {
|
||||||
|
this.actionable = actionable;
|
||||||
|
this.changeDetector.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
setBackEnabled(enabled: boolean): void {
|
||||||
|
this.backEnabled = enabled;
|
||||||
|
this.changeDetector.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitle(title: string) {
|
||||||
|
this.title = title;
|
||||||
|
this.changeDetector.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDarkModeChanges(darkMode: any) {
|
||||||
|
const themeColor = this.document.getElementsByName('theme-color')[0] as HTMLMetaElement;
|
||||||
|
let themeColorValue: string;
|
||||||
|
if (darkMode.matches) {
|
||||||
|
themeColorValue = '#333333';
|
||||||
} else {
|
} else {
|
||||||
this.loggedIn = false;
|
themeColorValue = '#F1F1F1';
|
||||||
}
|
}
|
||||||
}
|
themeColor.content = themeColorValue;
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
getUsername(): String {
|
|
||||||
return this.user.value.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
goBack(): void {
|
|
||||||
this.location.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
logout(): void {
|
|
||||||
this.twigsService.logout().subscribe(_ => {
|
|
||||||
this.location.go('/');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,20 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||||
import { MatListModule } from '@angular/material/list';
|
import { MatLegacyListModule as MatListModule } from '@angular/material/legacy-list';
|
||||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
import { MatLegacyProgressBarModule as MatProgressBarModule } from '@angular/material/legacy-progress-bar';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
|
||||||
import { MatRadioModule } from '@angular/material/radio';
|
import { MatLegacyRadioModule as MatRadioModule } from '@angular/material/legacy-radio';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { TransactionsComponent } from './transactions/transactions.component';
|
import { TransactionsComponent } from './transactions/transactions.component';
|
||||||
|
@ -38,27 +38,16 @@ import { NewBudgetComponent } from './budgets/new-budget/new-budget.component';
|
||||||
import { BudgetDetailsComponent } from './budgets/budget-details/budget-details.component';
|
import { BudgetDetailsComponent } from './budgets/budget-details/budget-details.component';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import { CurrencyMaskModule, CurrencyMaskConfig, CURRENCY_MASK_CONFIG } from 'ng2-currency-mask';
|
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||||
import { CategoryBreakdownComponent } from './categories/category-breakdown/category-breakdown.component';
|
import { CategoryBreakdownComponent } from './categories/category-breakdown/category-breakdown.component';
|
||||||
import { ChartsModule } from 'ng2-charts';
|
import { NgChartsModule } from 'ng2-charts';
|
||||||
import { TWIGS_SERVICE } from './shared/twigs.service';
|
import { TWIGS_SERVICE } from './shared/twigs.service';
|
||||||
import { AuthInterceptor } from './shared/auth.interceptor';
|
import { AuthInterceptor } from './shared/auth.interceptor';
|
||||||
import { TwigsHttpService } from './shared/twigs.http.service';
|
import { TwigsHttpService } from './shared/twigs.http.service';
|
||||||
import { TwigsLocalService } from './shared/twigs.local.service';
|
import { TwigsLocalService } from './shared/twigs.local.service';
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
|
||||||
import { TransactionListComponent } from './transactions/transaction-list/transaction-list.component';
|
import { TransactionListComponent } from './transactions/transaction-list/transaction-list.component';
|
||||||
import { EditCategoryComponent } from './categories/edit-category/edit-category.component';
|
import { EditCategoryComponent } from './categories/edit-category/edit-category.component';
|
||||||
|
import { EditBudgetComponent } from './budgets/edit-budget/edit-budget.component';
|
||||||
export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
|
||||||
align: 'left',
|
|
||||||
precision: 2,
|
|
||||||
prefix: '',
|
|
||||||
thousands: ',',
|
|
||||||
decimal: '.',
|
|
||||||
suffix: '',
|
|
||||||
allowNegative: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -83,6 +72,7 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
||||||
CategoryBreakdownComponent,
|
CategoryBreakdownComponent,
|
||||||
TransactionListComponent,
|
TransactionListComponent,
|
||||||
EditCategoryComponent,
|
EditCategoryComponent,
|
||||||
|
EditBudgetComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -103,17 +93,15 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
CurrencyMaskModule,
|
NgChartsModule,
|
||||||
ChartsModule,
|
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig },
|
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||||
{ provide: TWIGS_SERVICE, useClass: TwigsHttpService },
|
{ provide: TWIGS_SERVICE, useClass: TwigsHttpService },
|
||||||
|
{ provide: Storage, useValue: window.localStorage },
|
||||||
// { provide: TWIGS_SERVICE, useClass: TwigsLocalService },
|
// { provide: TWIGS_SERVICE, useClass: TwigsLocalService },
|
||||||
CookieService
|
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!isLoading && budget" class="form budget-form">
|
<div *ngIf="!isLoading && budget" class="form budget-form">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input matInput [(ngModel)]="budget.name" placeholder="Name" required>
|
<input matInput [(ngModel)]="budget.name" placeholder="Name" required autocapitalize="words">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<textarea matInput [(ngModel)]="budget.description" placeholder="Description"></textarea>
|
<textarea matInput [(ngModel)]="budget.description" placeholder="Description" autocapitalize="sentences"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button mat-button color="accent" (click)="save()">Save</button>
|
<button mat-raised-button color="accent" (click)="save()">Save</button>
|
||||||
<button class="button-delete" mat-button color="warn" *ngIf="budget.id" (click)="delete()">Delete</button>
|
<button class="button-delete" mat-raised-button color="warn" *ngIf="!create" (click)="delete()">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { AddEditBudgetComponent } from './add-edit-budget.component';
|
import { AddEditBudgetComponent } from './add-edit-budget.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('AddEditBudgetComponent', () => {
|
||||||
let component: AddEditBudgetComponent;
|
let component: AddEditBudgetComponent;
|
||||||
let fixture: ComponentFixture<AddEditBudgetComponent>;
|
let fixture: ComponentFixture<AddEditBudgetComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ AddEditBudgetComponent ]
|
declarations: [ AddEditBudgetComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Budget } from '../budget';
|
||||||
import { AppComponent } from 'src/app/app.component';
|
import { AppComponent } from 'src/app/app.component';
|
||||||
import { User, UserPermission, Permission } from 'src/app/users/user';
|
import { User, UserPermission, Permission } from 'src/app/users/user';
|
||||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-add-edit-budget',
|
selector: 'app-add-edit-budget',
|
||||||
|
@ -12,48 +13,53 @@ import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||||
export class AddEditBudgetComponent {
|
export class AddEditBudgetComponent {
|
||||||
@Input() title: string;
|
@Input() title: string;
|
||||||
@Input() budget: Budget;
|
@Input() budget: Budget;
|
||||||
|
@Input() create: boolean;
|
||||||
public users: UserPermission[];
|
public users: UserPermission[];
|
||||||
public searchedUsers: User[] = [];
|
public searchedUsers: User[] = [];
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private app: AppComponent,
|
private app: AppComponent,
|
||||||
|
private router: Router,
|
||||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||||
) {
|
) {
|
||||||
this.app.title = this.title;
|
this.app.setTitle(this.title)
|
||||||
this.app.backEnabled = true;
|
this.app.setBackEnabled(true);
|
||||||
this.users = [new UserPermission(this.app.user.value.id, Permission.OWNER)];
|
this.users = [new UserPermission(this.app.user.value.id, Permission.OWNER)];
|
||||||
}
|
}
|
||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
let observable;
|
let promise: Promise<Budget>;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
if (this.budget.id) {
|
if (this.create) {
|
||||||
// This is an existing transaction, update it
|
// This is a new budget, save it
|
||||||
observable = this.twigsService.updateBudget(this.budget.id, this.budget);
|
promise = this.twigsService.createBudget(
|
||||||
} else {
|
this.budget.id,
|
||||||
// This is a new transaction, save it
|
|
||||||
observable = this.twigsService.createBudget(
|
|
||||||
this.budget.name,
|
this.budget.name,
|
||||||
this.budget.description,
|
this.budget.description,
|
||||||
this.users
|
this.users
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// This is an existing budget, update it
|
||||||
|
promise = this.twigsService.updateBudget(this.budget.id, this.budget);
|
||||||
}
|
}
|
||||||
// TODO: Check if it was actually successful or not
|
// TODO: Check if it was actually successful or not
|
||||||
observable.subscribe(val => {
|
promise.then(_ => {
|
||||||
this.app.goBack();
|
this.app.goBack();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(): void {
|
delete(): void {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.twigsService.deleteBudget(this.budget.id);
|
this.twigsService.deleteBudget(this.budget.id)
|
||||||
this.app.goBack();
|
.then(() => {
|
||||||
|
this.router.navigateByUrl("/budgets");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement a search box with suggestions to add users
|
// TODO: Implement a search box with suggestions to add users
|
||||||
searchUsers(username: string) {
|
searchUsers(username: string) {
|
||||||
this.twigsService.getUsersByUsername(username).subscribe(users => {
|
this.twigsService.getUsersByUsername(username).then(users => {
|
||||||
this.searchedUsers = users;
|
this.searchedUsers = users;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,69 +1,111 @@
|
||||||
.dashboard {
|
.dashboard {
|
||||||
color: #F1F1F1;
|
color: #333333;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 1em;
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard>mat-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
max-width: 500px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard .dashboard-primary {
|
||||||
|
padding: 5em 1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-primary>* {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard div h2,
|
||||||
|
.dashboard div h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard p,
|
||||||
|
.dashboard a {
|
||||||
|
color: #333333;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-primary div {
|
||||||
|
bottom: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
left: 0.5em;
|
||||||
|
right: 0.5em;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard .no-categories {
|
||||||
|
padding: 1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard .no-categories a {
|
||||||
|
border-color: #333333;
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px dashed;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard .no-categories p {
|
||||||
|
line-height: normal;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.view-all {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5em;
|
||||||
|
top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1160px) {
|
||||||
|
mat-card {
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard > mat-card {
|
.category-info {
|
||||||
|
height: 313px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 610px) {
|
||||||
|
.dashboard {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard>mat-card {
|
||||||
|
margin: 1em auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.dashboard {
|
||||||
|
color: #F1F1F1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard>mat-card {
|
||||||
background: #212121;
|
background: #212121;
|
||||||
display: inline-block;
|
|
||||||
margin: 1em;
|
|
||||||
padding: 1em;
|
|
||||||
max-width: 500px;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard .dashboard-primary {
|
.dashboard p,
|
||||||
padding: 5em 1em;
|
.dashboard a {
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-primary > * {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard div h2, .dashboard div h3 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard p, .dashboard a {
|
|
||||||
color: #F1F1F1;
|
color: #F1F1F1;
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-primary div {
|
|
||||||
bottom: 0.5em;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
left: 0.5em;
|
|
||||||
right: 0.5em;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .no-categories {
|
|
||||||
padding: 1em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard .no-categories a {
|
.dashboard .no-categories a {
|
||||||
display: inline-block;
|
border-color: #F1F1F1;
|
||||||
border: 1px dashed #F1F1F1;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.dashboard .no-categories p {
|
|
||||||
line-height: normal;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.view-all {
|
|
||||||
position: absolute;
|
|
||||||
right: 0.5em;
|
|
||||||
top: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,19 +3,19 @@
|
||||||
<h2 class="balance">
|
<h2 class="balance">
|
||||||
Current Balance: <br />
|
Current Balance: <br />
|
||||||
<span
|
<span
|
||||||
[ngClass]="{'income': getBalance() > 0, 'expense': getBalance() < 0}">{{ getBalance() / 100 | currency }}</span>
|
[ngClass]="{'income': budgetBalance > 0, 'expense': budgetBalance < 0}">{{ budgetBalance / 100 | currency }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<app-category-breakdown [barChartLabels]="barChartLabels" [barChartData]="barChartData">
|
<app-category-breakdown [barChartLabels]="barChartLabels" [barChartData]="barChartData">
|
||||||
</app-category-breakdown>
|
</app-category-breakdown>
|
||||||
<div class="transaction-navigation">
|
<div class="transaction-navigation">
|
||||||
<a mat-button routerLink="/budgets/{{ budget.id }}/transactions" *ngIf="budget">View Transactions</a>
|
<a mat-button routerLink="/transactions" [queryParams]="{budgetIds: budget.id}" *ngIf="budget">View Transactions</a>
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="dashboard-categories" [hidden]="!budget">
|
<mat-card class="dashboard-categories" [hidden]="!budget">
|
||||||
<h3 class="categories">Income</h3>
|
<h3 class="categories">Income</h3>
|
||||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" class="view-all" *ngIf="budget">Add Category</a>
|
<a mat-button routerLink="/categories/new" [queryParams]="{budgetId: budget.id}" class="view-all" *ngIf="budget">Add Category</a>
|
||||||
<div class="no-categories" *ngIf="!income || income.length === 0">
|
<div class="no-categories" *ngIf="!income || income.length === 0">
|
||||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" *ngIf="budget">
|
<a mat-button routerLink="/categories/new" [queryParams]="{budgetId: budget.id}" *ngIf="budget">
|
||||||
<mat-icon>add</mat-icon>
|
<mat-icon>add</mat-icon>
|
||||||
<p>Add categories to gain more insights into your income.</p>
|
<p>Add categories to gain more insights into your income.</p>
|
||||||
</a>
|
</a>
|
||||||
|
@ -27,9 +27,9 @@
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="dashboard-categories" [hidden]="!budget">
|
<mat-card class="dashboard-categories" [hidden]="!budget">
|
||||||
<h3 class="categories">Expenses</h3>
|
<h3 class="categories">Expenses</h3>
|
||||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" class="view-all" *ngIf="budget">Add Category</a>
|
<a mat-button routerLink="/categories/new" [queryParams]="{budgetId: budget.id, expense: true}" class="view-all" *ngIf="budget">Add Category</a>
|
||||||
<div class="no-categories" *ngIf="!expenses || expenses.length === 0">
|
<div class="no-categories" *ngIf="!expenses || expenses.length === 0">
|
||||||
<a mat-button routerLink="/budgets/{{ budget.id }}/categories/new" *ngIf="budget">
|
<a mat-button routerLink="/categories/new" [queryParams]="{budgetId: budget.id, expense: true}" *ngIf="budget">
|
||||||
<mat-icon>add</mat-icon>
|
<mat-icon>add</mat-icon>
|
||||||
<p>Add categories to gain more insights into your expenses.</p>
|
<p>Add categories to gain more insights into your expenses.</p>
|
||||||
</a>
|
</a>
|
||||||
|
@ -40,6 +40,6 @@
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
<a mat-fab routerLink="/budgets/{{ budget.id }}/transactions/new" *ngIf="budget">
|
<a mat-fab routerLink="/transactions/new" [queryParams]="{budgetId: budget.id}" *ngIf="budget">
|
||||||
<mat-icon aria-label="Add">add</mat-icon>
|
<mat-icon aria-label="Add">add</mat-icon>
|
||||||
</a>
|
</a>
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { BudgetDetailsComponent } from './budget-details.component';
|
import { BudgetDetailsComponent } from './budget-details.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('BudgetDetailsComponent', () => {
|
||||||
let component: BudgetDetailsComponent;
|
let component: BudgetDetailsComponent;
|
||||||
let fixture: ComponentFixture<BudgetDetailsComponent>;
|
let fixture: ComponentFixture<BudgetDetailsComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ BudgetDetailsComponent ]
|
declarations: [ BudgetDetailsComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,53 +1,86 @@
|
||||||
import { Component, OnInit, Inject } from '@angular/core';
|
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
|
||||||
import { Budget } from '../budget';
|
import { Budget } from '../budget';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { AppComponent } from 'src/app/app.component';
|
import { AppComponent } from 'src/app/app.component';
|
||||||
import { Transaction } from 'src/app/transactions/transaction';
|
import { Transaction } from 'src/app/transactions/transaction';
|
||||||
import { Category } from 'src/app/categories/category';
|
import { Category } from 'src/app/categories/category';
|
||||||
import { Observable } from 'rxjs';
|
import { ChartDataset } from 'chart.js';
|
||||||
import { Label } from 'ng2-charts';
|
|
||||||
import { ChartDataSets } from 'chart.js';
|
|
||||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||||
|
import { Actionable } from '../../shared/actionable';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-budget-details',
|
selector: 'app-budget-details',
|
||||||
templateUrl: './budget-details.component.html',
|
templateUrl: './budget-details.component.html',
|
||||||
styleUrls: ['./budget-details.component.css']
|
styleUrls: ['./budget-details.component.css']
|
||||||
})
|
})
|
||||||
export class BudgetDetailsComponent implements OnInit {
|
export class BudgetDetailsComponent implements OnInit, OnDestroy, Actionable {
|
||||||
|
|
||||||
budget: Budget;
|
budget: Budget;
|
||||||
|
public budgetBalance: number;
|
||||||
public transactions: Transaction[];
|
public transactions: Transaction[];
|
||||||
public expenses: Category[] = [];
|
public expenses: Category[] = [];
|
||||||
public income: Category[] = [];
|
public income: Category[] = [];
|
||||||
categoryBalances: Map<number, number>;
|
categoryBalances: Map<string, number>;
|
||||||
expectedIncome = 0;
|
expectedIncome = 0;
|
||||||
actualIncome = 0;
|
actualIncome = 0;
|
||||||
expectedExpenses = 0;
|
expectedExpenses = 0;
|
||||||
actualExpenses = 0;
|
actualExpenses = 0;
|
||||||
barChartLabels: Label[] = ['Income', 'Expenses'];
|
barChartLabels: string[] = ['Income', 'Expenses'];
|
||||||
barChartData: ChartDataSets[] = [
|
barChartData: ChartDataset[] = [
|
||||||
{ data: [0, 0], label: 'Expected' },
|
{ data: [0, 0], label: 'Expected' },
|
||||||
{ data: [0, 0], label: 'Actual' },
|
{ data: [0, 0], label: 'Actual' },
|
||||||
];
|
];
|
||||||
|
from: Date
|
||||||
|
to: Date
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private app: AppComponent,
|
private app: AppComponent,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||||
) { }
|
private router: Router,
|
||||||
|
) {
|
||||||
|
let fromStr = this.route.snapshot.queryParamMap.get('from');
|
||||||
|
if (fromStr) {
|
||||||
|
let fromDate = new Date(fromStr);
|
||||||
|
if (!isNaN(fromDate.getTime())) {
|
||||||
|
this.from = fromDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.from) {
|
||||||
|
let date = new Date();
|
||||||
|
date.setHours(0);
|
||||||
|
date.setMinutes(0);
|
||||||
|
date.setSeconds(0);
|
||||||
|
date.setMilliseconds(0);
|
||||||
|
date.setDate(1);
|
||||||
|
this.from = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
let toStr = this.route.snapshot.queryParamMap.get('to');
|
||||||
|
if (toStr) {
|
||||||
|
let toDate = new Date(toStr);
|
||||||
|
if (!isNaN(toDate.getTime())) {
|
||||||
|
this.to = toDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.getBudget();
|
this.getBudget();
|
||||||
this.app.backEnabled = false;
|
this.app.setBackEnabled(false);
|
||||||
|
this.app.setActionable(this)
|
||||||
this.categoryBalances = new Map();
|
this.categoryBalances = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.app.setActionable(null)
|
||||||
|
}
|
||||||
|
|
||||||
getBudget() {
|
getBudget() {
|
||||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
const id = this.route.snapshot.paramMap.get('id');
|
||||||
this.twigsService.getBudget(id)
|
this.twigsService.getBudget(id)
|
||||||
.subscribe(budget => {
|
.then(budget => {
|
||||||
this.app.title = budget.name;
|
this.app.setTitle(budget.name)
|
||||||
this.budget = budget;
|
this.budget = budget;
|
||||||
this.getBalance();
|
this.getBalance();
|
||||||
this.getTransactions();
|
this.getTransactions();
|
||||||
|
@ -77,15 +110,12 @@ export class BudgetDetailsComponent implements OnInit {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getBalance(): number {
|
getBalance(): void {
|
||||||
let totalBalance = 0;
|
const id = this.route.snapshot.paramMap.get('id');
|
||||||
if (!this.categoryBalances) {
|
this.twigsService.getBudgetBalance(id, this.from, this.to)
|
||||||
return 0;
|
.then(balance => {
|
||||||
}
|
this.budgetBalance = balance;
|
||||||
this.categoryBalances.forEach(balance => {
|
});
|
||||||
totalBalance += balance;
|
|
||||||
});
|
|
||||||
return totalBalance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTransactions(): void {
|
getTransactions(): void {
|
||||||
|
@ -96,67 +126,48 @@ export class BudgetDetailsComponent implements OnInit {
|
||||||
date.setMilliseconds(0);
|
date.setMilliseconds(0);
|
||||||
date.setDate(1);
|
date.setDate(1);
|
||||||
this.twigsService.getTransactions(this.budget.id, null, 5, date)
|
this.twigsService.getTransactions(this.budget.id, null, 5, date)
|
||||||
.subscribe(transactions => this.transactions = <Transaction[]>transactions);
|
.then(transactions => this.transactions = <Transaction[]>transactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategories(): void {
|
async getCategories() {
|
||||||
this.twigsService.getCategories(this.budget.id).subscribe(categories => {
|
const categories = await this.twigsService.getCategories(this.budget.id)
|
||||||
const categoryBalances = new Map<number, number>();
|
const categoryBalances = new Map<string, number>();
|
||||||
let categoryBalancesCount = 0;
|
let categoryBalancesCount = 0;
|
||||||
console.log(categories);
|
for (const category of categories) {
|
||||||
for (const category of categories) {
|
if (category.expense) {
|
||||||
if (category.expense) {
|
this.expenses.push(category);
|
||||||
this.expenses.push(category);
|
this.expectedExpenses += category.amount;
|
||||||
this.expectedExpenses += category.amount;
|
} else {
|
||||||
} else {
|
this.income.push(category);
|
||||||
this.income.push(category);
|
this.expectedIncome += category.amount;
|
||||||
this.expectedIncome += category.amount;
|
|
||||||
}
|
|
||||||
this.getCategoryBalance(category.id).subscribe(
|
|
||||||
balance => {
|
|
||||||
console.log(balance);
|
|
||||||
if (category.expense) {
|
|
||||||
this.actualExpenses += balance * -1;
|
|
||||||
} else {
|
|
||||||
this.actualIncome += balance;
|
|
||||||
}
|
|
||||||
categoryBalances.set(category.id, balance);
|
|
||||||
categoryBalancesCount++;
|
|
||||||
},
|
|
||||||
error => { categoryBalancesCount++; },
|
|
||||||
() => {
|
|
||||||
// This weird workaround is to force the OnChanges callback to be fired.
|
|
||||||
// Angular needs the reference to the object to change in order for it to
|
|
||||||
// work.
|
|
||||||
if (categoryBalancesCount === categories.length) {
|
|
||||||
this.categoryBalances = categoryBalances;
|
|
||||||
this.updateBarChart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
try {
|
||||||
|
const balance = await this.twigsService.getCategoryBalance(category.id, this.from, this.to)
|
||||||
|
console.log(balance);
|
||||||
|
if (category.expense) {
|
||||||
|
this.actualExpenses += balance * -1;
|
||||||
|
} else {
|
||||||
|
this.actualIncome += balance;
|
||||||
|
}
|
||||||
|
categoryBalances.set(category.id, balance);
|
||||||
|
if (categoryBalancesCount === categories.length - 1) {
|
||||||
|
// This weird workaround is to force the OnChanges callback to be fired.
|
||||||
|
// Angular needs the reference to the object to change in order for it to
|
||||||
|
// work.
|
||||||
|
this.categoryBalances = categoryBalances;
|
||||||
|
this.updateBarChart();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
categoryBalancesCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategoryBalance(category: number): Observable<number> {
|
doAction(): void {
|
||||||
return Observable.create(subscriber => {
|
this.router.navigateByUrl(this.router.routerState.snapshot.url + "/edit")
|
||||||
let date = new Date();
|
}
|
||||||
date.setHours(0);
|
|
||||||
date.setMinutes(0);
|
getActionLabel(): string {
|
||||||
date.setSeconds(0);
|
return "Edit";
|
||||||
date.setDate(1);
|
|
||||||
this.twigsService.getTransactions(this.budget.id, category, null, date).subscribe(transactions => {
|
|
||||||
let balance = 0;
|
|
||||||
for (const transaction of transactions) {
|
|
||||||
if (transaction.expense) {
|
|
||||||
balance -= transaction.amount;
|
|
||||||
} else {
|
|
||||||
balance += transaction.amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
subscriber.next(balance);
|
|
||||||
subscriber.complete();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
.dashboard {
|
.dashboard {
|
||||||
color: #F1F1F1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { BudgetsComponent } from './budget.component';
|
import { BudgetsComponent } from './budget.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('BudgetsComponent', () => {
|
||||||
let component: BudgetsComponent;
|
let component: BudgetsComponent;
|
||||||
let fixture: ComponentFixture<BudgetsComponent>;
|
let fixture: ComponentFixture<BudgetsComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ BudgetsComponent ]
|
declarations: [ BudgetsComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,27 +20,29 @@ export class BudgetsComponent implements OnInit {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.app.backEnabled = false;
|
this.app.setBackEnabled(false);
|
||||||
this.app.title = 'Budgets';
|
|
||||||
this.app.user.subscribe(
|
this.app.user.subscribe(
|
||||||
user => {
|
user => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.loggedIn = false;
|
this.loggedIn = false;
|
||||||
|
this.app.setTitle('Welcome')
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.app.setTitle('Budgets')
|
||||||
this.loggedIn = true;
|
this.loggedIn = true;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.twigsService.getBudgets().subscribe(
|
this.twigsService.getBudgets()
|
||||||
budgets => {
|
.then(
|
||||||
console.log(budgets)
|
budgets => {
|
||||||
this.budgets = budgets;
|
console.log(budgets)
|
||||||
|
this.budgets = budgets;
|
||||||
|
this.loading = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error)
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
});
|
||||||
error => {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { User } from '../users/user';
|
import { UserPermission } from '../users/user';
|
||||||
|
import { randomId } from '../shared/utils';
|
||||||
|
|
||||||
export class Budget {
|
export class Budget {
|
||||||
id: number;
|
id: string = randomId();
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
users: User[];
|
users: UserPermission[];
|
||||||
}
|
}
|
||||||
|
|
0
src/app/budgets/edit-budget/edit-budget.component.css
Normal file
0
src/app/budgets/edit-budget/edit-budget.component.css
Normal file
1
src/app/budgets/edit-budget/edit-budget.component.html
Normal file
1
src/app/budgets/edit-budget/edit-budget.component.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<app-add-edit-budget [title]="'Edit Budget'" [budget]="budget" [create]="false"></app-add-edit-budget>
|
25
src/app/budgets/edit-budget/edit-budget.component.spec.ts
Normal file
25
src/app/budgets/edit-budget/edit-budget.component.spec.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { EditBudgetComponent } from './edit-budget.component';
|
||||||
|
|
||||||
|
describe('EditBudgetComponent', () => {
|
||||||
|
let component: EditBudgetComponent;
|
||||||
|
let fixture: ComponentFixture<EditBudgetComponent>;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ EditBudgetComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditBudgetComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
27
src/app/budgets/edit-budget/edit-budget.component.ts
Normal file
27
src/app/budgets/edit-budget/edit-budget.component.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { TwigsService, TWIGS_SERVICE } from '../../shared/twigs.service';
|
||||||
|
import { Budget } from '../budget';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit-budget',
|
||||||
|
templateUrl: './edit-budget.component.html',
|
||||||
|
styleUrls: ['./edit-budget.component.css']
|
||||||
|
})
|
||||||
|
export class EditBudgetComponent implements OnInit {
|
||||||
|
|
||||||
|
budget: Budget;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const id = this.route.snapshot.paramMap.get('id');
|
||||||
|
this.twigsService.getBudget(id)
|
||||||
|
.then(budget => {
|
||||||
|
this.budget = budget;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
<app-add-edit-budget [title]="'Add Budget'" [budget]="budget"></app-add-edit-budget>
|
<app-add-edit-budget [title]="'Add Budget'" [budget]="budget" [create]="true"></app-add-edit-budget>
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { NewBudgetComponent } from './new-budget.component';
|
import { NewBudgetComponent } from './new-budget.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('NewBudgetComponent', () => {
|
||||||
let component: NewBudgetComponent;
|
let component: NewBudgetComponent;
|
||||||
let fixture: ComponentFixture<NewBudgetComponent>;
|
let fixture: ComponentFixture<NewBudgetComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ NewBudgetComponent ]
|
declarations: [ NewBudgetComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { CategoriesComponent } from './categories.component';
|
import { CategoriesComponent } from './categories.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('CategoriesComponent', () => {
|
||||||
let component: CategoriesComponent;
|
let component: CategoriesComponent;
|
||||||
let fixture: ComponentFixture<CategoriesComponent>;
|
let fixture: ComponentFixture<CategoriesComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ CategoriesComponent ]
|
declarations: [ CategoriesComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { Component, OnInit, Input, Inject } from '@angular/core';
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
import { Category } from './category';
|
import { Category } from './category';
|
||||||
import { AppComponent } from '../app.component';
|
import { AppComponent } from '../app.component';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { TransactionType } from '../transactions/transaction.type';
|
|
||||||
import { Budget } from '../budgets/budget';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { TWIGS_SERVICE, TwigsService } from '../shared/twigs.service';
|
import { TWIGS_SERVICE, TwigsService } from '../shared/twigs.service';
|
||||||
|
import { Transaction } from '../transactions/transaction';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-categories',
|
selector: 'app-categories',
|
||||||
|
@ -14,9 +12,9 @@ import { TWIGS_SERVICE, TwigsService } from '../shared/twigs.service';
|
||||||
})
|
})
|
||||||
export class CategoriesComponent implements OnInit {
|
export class CategoriesComponent implements OnInit {
|
||||||
|
|
||||||
budgetId: number;
|
budgetId: string;
|
||||||
public categories: Category[];
|
public categories: Category[];
|
||||||
public categoryBalances: Map<number, number>;
|
public categoryBalances: Map<string, number>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
@ -25,35 +23,39 @@ export class CategoriesComponent implements OnInit {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.budgetId = Number.parseInt(this.route.snapshot.paramMap.get('budgetId'));
|
this.budgetId = this.route.snapshot.paramMap.get('budgetId');
|
||||||
this.app.title = 'Categories';
|
this.app.setTitle('Categories')
|
||||||
this.app.backEnabled = true;
|
this.app.setBackEnabled(true);
|
||||||
this.getCategories();
|
this.getCategories();
|
||||||
this.categoryBalances = new Map();
|
this.categoryBalances = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategories(): void {
|
getCategories(): void {
|
||||||
this.twigsService.getCategories(this.budgetId).subscribe(categories => {
|
this.twigsService.getCategories(this.budgetId).then(categories => {
|
||||||
this.categories = categories;
|
this.categories = categories;
|
||||||
for (const category of this.categories) {
|
for (const category of this.categories) {
|
||||||
this.getCategoryBalance(category).subscribe(balance => this.categoryBalances.set(category.id, balance));
|
this.getCategoryBalance(category).then(balance => this.categoryBalances.set(category.id, balance));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategoryBalance(category: Category): Observable<number> {
|
getCategoryBalance(category: Category): Promise<number> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise(async (resolve, reject) => {
|
||||||
this.twigsService.getTransactions(this.budgetId, category.id).subscribe(transactions => {
|
let transactions: Transaction[]
|
||||||
let balance = 0;
|
try {
|
||||||
for (const transaction of transactions) {
|
transactions = await this.twigsService.getTransactions(this.budgetId, category.id)
|
||||||
if (transaction.expense) {
|
} catch(e) {
|
||||||
balance -= transaction.amount;
|
reject(e)
|
||||||
} else {
|
}
|
||||||
balance += transaction.amount;
|
let balance = 0;
|
||||||
}
|
for (const transaction of transactions) {
|
||||||
|
if (transaction.expense) {
|
||||||
|
balance -= transaction.amount;
|
||||||
|
} else {
|
||||||
|
balance += transaction.amount;
|
||||||
}
|
}
|
||||||
subscriber.next(balance);
|
}
|
||||||
});
|
resolve(balance);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
[options]="barChartOptions"
|
[options]="barChartOptions"
|
||||||
[labels]="barChartLabels"
|
[labels]="barChartLabels"
|
||||||
[legend]="barChartLegend"
|
[legend]="barChartLegend"
|
||||||
[chartType]="barChartType">
|
[type]="barChartType">
|
||||||
</canvas>
|
</canvas>
|
||||||
</div>
|
</div>
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { CategoryBreakdownComponent } from './category-breakdown.component';
|
import { CategoryBreakdownComponent } from './category-breakdown.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('CategoryBreakdownComponent', () => {
|
||||||
let component: CategoryBreakdownComponent;
|
let component: CategoryBreakdownComponent;
|
||||||
let fixture: ComponentFixture<CategoryBreakdownComponent>;
|
let fixture: ComponentFixture<CategoryBreakdownComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ CategoryBreakdownComponent ]
|
declarations: [ CategoryBreakdownComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, SimpleChange, ViewChild } from '@angular/core';
|
import { Component, OnInit, Input, OnChanges, SimpleChanges, SimpleChange, ViewChild } from '@angular/core';
|
||||||
import { Category } from '../category';
|
import { Category } from '../category';
|
||||||
import { CategoriesComponent } from '../categories.component';
|
import { CategoriesComponent } from '../categories.component';
|
||||||
import { ChartOptions, ChartType, ChartDataSets } from 'chart.js';
|
import { ChartConfiguration, ChartType, ChartDataset } from 'chart.js';
|
||||||
import { BaseChartDirective, Label } from 'ng2-charts';
|
import { BaseChartDirective } from 'ng2-charts';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-category-breakdown',
|
selector: 'app-category-breakdown',
|
||||||
|
@ -10,22 +10,24 @@ import { BaseChartDirective, Label } from 'ng2-charts';
|
||||||
styleUrls: ['./category-breakdown.component.css']
|
styleUrls: ['./category-breakdown.component.css']
|
||||||
})
|
})
|
||||||
export class CategoryBreakdownComponent implements OnInit, OnChanges {
|
export class CategoryBreakdownComponent implements OnInit, OnChanges {
|
||||||
barChartOptions: ChartOptions = {
|
barChartOptions: ChartConfiguration['options'] = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
scales: {
|
scales: {
|
||||||
xAxes: [{
|
x: {
|
||||||
ticks: {
|
ticks: {
|
||||||
beginAtZero: true
|
// beginAtZero: true
|
||||||
}
|
}
|
||||||
}], yAxes: [{}]
|
},
|
||||||
|
y: {}
|
||||||
},
|
},
|
||||||
|
indexAxis: 'y'
|
||||||
};
|
};
|
||||||
@Input() barChartLabels: Label[];
|
@Input() barChartLabels: string[];
|
||||||
@Input() barChartData: ChartDataSets[] = [
|
@Input() barChartData: ChartDataset[] = [
|
||||||
{ data: [0, 0, 0, 0], label: '' },
|
{ data: [0, 0, 0, 0], label: '' },
|
||||||
];
|
];
|
||||||
barChartType: ChartType = 'horizontalBar';
|
barChartType: ChartType = 'bar';
|
||||||
barChartLegend = true;
|
barChartLegend = true;
|
||||||
@ViewChild(BaseChartDirective) chart: BaseChartDirective;
|
@ViewChild(BaseChartDirective) chart: BaseChartDirective;
|
||||||
|
|
||||||
|
@ -35,9 +37,9 @@ export class CategoryBreakdownComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
console.log(changes);
|
console.log(changes);
|
||||||
if (changes.barChartLabels) {
|
// if (changes.barChartLabels) {
|
||||||
this.barChartLabels = changes.barChartLabels.currentValue;
|
// this.barChartLabels = changes.barChartLabels.currentValue;
|
||||||
}
|
// }
|
||||||
if (changes.barChartData) {
|
if (changes.barChartData) {
|
||||||
this.barChartData = changes.barChartData.currentValue;
|
this.barChartData = changes.barChartData.currentValue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
.category-description {
|
||||||
|
padding: 0 1em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
<app-transaction-list *ngIf="budgetId && category" [budgetId]="budgetId" [categoryId]="category.id"></app-transaction-list>
|
<p class="category-description" *ngIf="category && category.description" [innerHtml]="category.description"></p>
|
||||||
<a mat-fab routerLink="/budgets/{{ budgetId }}/transactions/new">
|
<app-transaction-list *ngIf="budgetId && category" [budgetIds]="[budgetId]" [categoryIds]="[category.id]"></app-transaction-list>
|
||||||
|
<a mat-fab routerLink="/transactions/new" [queryParams]="{budgetId: budgetId}">
|
||||||
<mat-icon aria-label="Add">add</mat-icon>
|
<mat-icon aria-label="Add">add</mat-icon>
|
||||||
</a>
|
</a>
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { CategoryDetailsComponent } from './category-details.component';
|
import { CategoryDetailsComponent } from './category-details.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('CategoryDetailsComponent', () => {
|
||||||
let component: CategoryDetailsComponent;
|
let component: CategoryDetailsComponent;
|
||||||
let fixture: ComponentFixture<CategoryDetailsComponent>;
|
let fixture: ComponentFixture<CategoryDetailsComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ CategoryDetailsComponent ]
|
declarations: [ CategoryDetailsComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { Actionable } from '../../shared/actionable';
|
||||||
})
|
})
|
||||||
export class CategoryDetailsComponent implements OnInit, OnDestroy, Actionable {
|
export class CategoryDetailsComponent implements OnInit, OnDestroy, Actionable {
|
||||||
|
|
||||||
budgetId: number;
|
budgetId: string;
|
||||||
category: Category;
|
category: Category;
|
||||||
public transactions: Transaction[];
|
public transactions: Transaction[];
|
||||||
|
|
||||||
|
@ -33,21 +33,21 @@ export class CategoryDetailsComponent implements OnInit, OnDestroy, Actionable {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.app.backEnabled = true;
|
this.app.setBackEnabled(true);
|
||||||
this.app.actionable = this;
|
this.app.setActionable(this)
|
||||||
this.getCategory();
|
this.getCategory();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.app.actionable = null;
|
this.app.setActionable(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategory(): void {
|
getCategory(): void {
|
||||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
const id = this.route.snapshot.paramMap.get('id');
|
||||||
this.twigsService.getCategory(id)
|
this.twigsService.getCategory(id)
|
||||||
.subscribe(category => {
|
.then(category => {
|
||||||
category.amount /= 100;
|
category.amount /= 100;
|
||||||
this.app.title = category.title;
|
this.app.setTitle(category.title)
|
||||||
this.category = category;
|
this.category = category;
|
||||||
this.budgetId = category.budgetId;
|
this.budgetId = category.budgetId;
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,11 +2,14 @@
|
||||||
<p>Select a category from the list to view details about it or edit it.</p>
|
<p>Select a category from the list to view details about it or edit it.</p>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="currentCategory" class="form category-form">
|
<div *ngIf="currentCategory" class="form category-form">
|
||||||
<mat-form-field (keyup.enter)="doAction()">
|
<mat-form-field (keyup.enter)="save()">
|
||||||
<input matInput [(ngModel)]="currentCategory.title" placeholder="Name" required>
|
<input matInput [(ngModel)]="currentCategory.title" placeholder="Name" required autocapitalize="words">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field (keyup.enter)="doAction()">
|
<mat-form-field (keyup.enter)="save()">
|
||||||
<input matInput type="text" [(ngModel)]="currentCategory.amount" placeholder="Amount" required currencyMask>
|
<textarea matInput [(ngModel)]="currentCategory.description" placeholder="Description" autocapitalize="sentences"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field (keyup.enter)="save()">
|
||||||
|
<input matInput type="input" [(ngModel)]="currentCategory.amount" placeholder="Amount" required step="0.01">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-radio-group [(ngModel)]="currentCategory.expense">
|
<mat-radio-group [(ngModel)]="currentCategory.expense">
|
||||||
<mat-radio-button [value]="true">Expense</mat-radio-button>
|
<mat-radio-button [value]="true">Expense</mat-radio-button>
|
||||||
|
@ -18,6 +21,6 @@
|
||||||
<input type="color" matInput [(ngModel)]="currentCategory.color" placeholder="Color">
|
<input type="color" matInput [(ngModel)]="currentCategory.color" placeholder="Color">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
-->
|
-->
|
||||||
<button mat-button color="accent" (click)="save()">Save</button>
|
<button mat-raised-button color="accent" (click)="save()">Save</button>
|
||||||
<button class="button-delete" mat-button color="warn" *ngIf="currentCategory.id" (click)="delete()">Delete</button>
|
<button class="button-delete" mat-raised-button color="warn" *ngIf="!create" (click)="delete()">Delete</button>
|
||||||
</div>
|
</div>
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { CategoryFormComponent } from './category-form.component';
|
import { CategoryFormComponent } from './category-form.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('CategoryFormComponent', () => {
|
||||||
let component: CategoryFormComponent;
|
let component: CategoryFormComponent;
|
||||||
let fixture: ComponentFixture<CategoryFormComponent>;
|
let fixture: ComponentFixture<CategoryFormComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ CategoryFormComponent ]
|
declarations: [ CategoryFormComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Component, OnInit, Input, OnDestroy, Inject } from '@angular/core';
|
import { Component, OnInit, Input, Inject } from '@angular/core';
|
||||||
import { Category } from '../category';
|
import { Category } from '../category';
|
||||||
import { AppComponent } from 'src/app/app.component';
|
import { AppComponent } from 'src/app/app.component';
|
||||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||||
|
import { decimalToInteger } from 'src/app/shared/utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-category-form',
|
selector: 'app-category-form',
|
||||||
|
@ -10,9 +11,10 @@ import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||||
})
|
})
|
||||||
export class CategoryFormComponent implements OnInit {
|
export class CategoryFormComponent implements OnInit {
|
||||||
|
|
||||||
@Input() budgetId: number;
|
@Input() budgetId: string;
|
||||||
@Input() title: string;
|
@Input() title: string;
|
||||||
@Input() currentCategory: Category;
|
@Input() currentCategory: Category;
|
||||||
|
@Input() create: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private app: AppComponent,
|
private app: AppComponent,
|
||||||
|
@ -20,40 +22,40 @@ export class CategoryFormComponent implements OnInit {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.app.backEnabled = true;
|
this.app.setBackEnabled(true);
|
||||||
this.app.title = this.title;
|
this.app.setTitle(this.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
let observable;
|
let promise;
|
||||||
if (this.currentCategory.id) {
|
this.currentCategory.amount = decimalToInteger(String(this.currentCategory.amount))
|
||||||
// This is an existing category, update it
|
if (this.create) {
|
||||||
observable = this.twigsService.updateCategory(
|
|
||||||
this.budgetId,
|
|
||||||
this.currentCategory.id,
|
|
||||||
{
|
|
||||||
name: this.currentCategory.title,
|
|
||||||
amount: this.currentCategory.amount * 100,
|
|
||||||
expense: this.currentCategory.expense,
|
|
||||||
archived: this.currentCategory.archived
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// This is a new category, save it
|
// This is a new category, save it
|
||||||
observable = this.twigsService.createCategory(
|
promise = this.twigsService.createCategory(
|
||||||
|
this.currentCategory.id,
|
||||||
this.budgetId,
|
this.budgetId,
|
||||||
this.currentCategory.title,
|
this.currentCategory.title,
|
||||||
this.currentCategory.amount * 100,
|
this.currentCategory.description,
|
||||||
|
this.currentCategory.amount,
|
||||||
this.currentCategory.expense
|
this.currentCategory.expense
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// This is an existing category, update it
|
||||||
|
const updatedCategory: Category = {
|
||||||
|
...this.currentCategory,
|
||||||
|
}
|
||||||
|
promise = this.twigsService.updateCategory(
|
||||||
|
this.currentCategory.id,
|
||||||
|
this.currentCategory
|
||||||
|
);
|
||||||
}
|
}
|
||||||
observable.subscribe(val => {
|
promise.then(_ => {
|
||||||
this.app.goBack();
|
this.app.goBack();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(): void {
|
delete(): void {
|
||||||
this.twigsService.deleteCategory(this.budgetId, this.currentCategory.id).subscribe(() => {
|
this.twigsService.deleteCategory(this.currentCategory.id).then(() => {
|
||||||
this.app.goBack();
|
this.app.goBack();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
.categories mat-progress-bar.mat-progress-bar {
|
.categories mat-progress-bar.mat-progress-bar {
|
||||||
background-color: #BDBDBD;
|
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .mat-progress-bar-buffer {
|
|
||||||
background-color: #BDBDBD;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.mat-line.category-list-title {
|
p.mat-line.category-list-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -26,5 +21,11 @@ p.mat-line.category-list-title .remaining {
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .mat-progress-bar-buffer {
|
::ng-deep .mat-progress-bar-buffer {
|
||||||
background-color: #333333 !important;
|
background-color: #F1F1F1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
::ng-deep .mat-progress-bar-buffer {
|
||||||
|
background-color: #333333 !important;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
<mat-nav-list class="categories">
|
<mat-nav-list class="categories">
|
||||||
<a mat-list-item *ngFor="let category of categories" routerLink="/budgets/{{ budgetId }}/categories/{{ category.id }}">
|
<a mat-list-item *ngFor="let category of categories" routerLink="/categories/{{ category.id }}">
|
||||||
<p matLine class="category-list-title">
|
<p matLine class="category-list-title">
|
||||||
<span>
|
<span>
|
||||||
{{ category.title }}
|
{{ category.title }}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { CategoryListComponent } from './category-list.component';
|
import { CategoryListComponent } from './category-list.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('CategoryListComponent', () => {
|
||||||
let component: CategoryListComponent;
|
let component: CategoryListComponent;
|
||||||
let fixture: ComponentFixture<CategoryListComponent>;
|
let fixture: ComponentFixture<CategoryListComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ CategoryListComponent ]
|
declarations: [ CategoryListComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,7 @@ export class CategoryListComponent implements OnInit {
|
||||||
|
|
||||||
@Input() budgetId: string;
|
@Input() budgetId: string;
|
||||||
@Input() categories: Category[];
|
@Input() categories: Category[];
|
||||||
@Input() categoryBalances: Map<number, number>;
|
@Input() categoryBalances: Map<string, number>;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
import { randomId } from '../shared/utils';
|
||||||
|
|
||||||
export class Category {
|
export class Category {
|
||||||
id: number;
|
id: string = randomId();
|
||||||
title: string;
|
title: string;
|
||||||
|
description: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
expense: boolean;
|
expense: boolean;
|
||||||
archived: boolean;
|
archived: boolean;
|
||||||
budgetId: number;
|
budgetId: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<app-category-form [title]="'Edit Category'" [budgetId]="budgetId" [currentCategory]="category"></app-category-form>
|
<app-category-form [title]="'Edit Category'" [budgetId]="budgetId" [currentCategory]="category" [create]="false"></app-category-form>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { EditCategoryComponent } from './edit-category.component';
|
import { EditCategoryComponent } from './edit-category.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('EditCategoryComponent', () => {
|
||||||
let component: EditCategoryComponent;
|
let component: EditCategoryComponent;
|
||||||
let fixture: ComponentFixture<EditCategoryComponent>;
|
let fixture: ComponentFixture<EditCategoryComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ EditCategoryComponent ]
|
declarations: [ EditCategoryComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { TWIGS_SERVICE, TwigsService } from '../../shared/twigs.service';
|
||||||
})
|
})
|
||||||
export class EditCategoryComponent implements OnInit {
|
export class EditCategoryComponent implements OnInit {
|
||||||
|
|
||||||
budgetId: number;
|
budgetId: string;
|
||||||
category: Category;
|
category: Category;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -21,16 +21,16 @@ export class EditCategoryComponent implements OnInit {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.app.backEnabled = true;
|
this.app.setBackEnabled(true);
|
||||||
this.getCategory();
|
this.getCategory();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategory(): void {
|
getCategory(): void {
|
||||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
const id = this.route.snapshot.paramMap.get('id');
|
||||||
this.twigsService.getCategory(id)
|
this.twigsService.getCategory(id)
|
||||||
.subscribe(category => {
|
.then(category => {
|
||||||
category.amount /= 100;
|
category.amount /= 100;
|
||||||
this.app.title = category.title;
|
this.app.setTitle(category.title)
|
||||||
this.category = category;
|
this.category = category;
|
||||||
this.budgetId = category.budgetId;
|
this.budgetId = category.budgetId;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<app-category-form [title]="'Add Category'" [budgetId]="budgetId" [currentCategory]="category"></app-category-form>
|
<app-category-form [title]="'Add Category'" [budgetId]="budgetId" [currentCategory]="category" [create]="true"></app-category-form>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { NewCategoryComponent } from './new-category.component';
|
import { NewCategoryComponent } from './new-category.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('NewCategoryComponent', () => {
|
||||||
let component: NewCategoryComponent;
|
let component: NewCategoryComponent;
|
||||||
let fixture: ComponentFixture<NewCategoryComponent>;
|
let fixture: ComponentFixture<NewCategoryComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ NewCategoryComponent ]
|
declarations: [ NewCategoryComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,7 +17,8 @@ export class NewCategoryComponent implements OnInit {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.budgetId = this.route.snapshot.paramMap.get('budgetId');
|
this.budgetId = this.route.snapshot.queryParamMap.get('budgetId');
|
||||||
|
console.log(`Creating category for budget ${this.budgetId}`)
|
||||||
this.category = new Category();
|
this.category = new Category();
|
||||||
// TODO: Set random color for category, improve color picker
|
// TODO: Set random color for category, improve color picker
|
||||||
// this.category.color =
|
// this.category.color =
|
||||||
|
|
221
src/app/recurringtransactions/recurringtransaction.ts
Normal file
221
src/app/recurringtransactions/recurringtransaction.ts
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
export class RecurringTransaction {
|
||||||
|
id: string = '';
|
||||||
|
title: string;
|
||||||
|
description?: string = null;
|
||||||
|
frequency: Frequency;
|
||||||
|
start: Date = new Date();
|
||||||
|
end?: Date;
|
||||||
|
amount: number;
|
||||||
|
expense = true;
|
||||||
|
categoryId: string;
|
||||||
|
budgetId: string;
|
||||||
|
createdBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Frequency {
|
||||||
|
unit: FrequencyUnit;
|
||||||
|
count: number;
|
||||||
|
time: Time;
|
||||||
|
amount?: (void | Set<DayOfWeek> | DayOfMonth | DayOfYear);
|
||||||
|
|
||||||
|
private constructor(unit: FrequencyUnit, count: number, time: Time, amount?: (void | Set<DayOfWeek> | DayOfMonth | DayOfYear)) {
|
||||||
|
this.unit = unit;
|
||||||
|
this.count = count;
|
||||||
|
this.time = time;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Daily(count: number, time: Time): Frequency {
|
||||||
|
return new Frequency(FrequencyUnit.DAILY, count, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Weekly(count: number, time: Time, daysOfWeek: Set<DayOfWeek>): Frequency {
|
||||||
|
return new Frequency(FrequencyUnit.WEEKLY, count, time, daysOfWeek)
|
||||||
|
}
|
||||||
|
|
||||||
|
static Monthly(count: number, time: Time, dayOfMonth: DayOfMonth): Frequency {
|
||||||
|
return new Frequency(FrequencyUnit.MONTHLY, count, time, dayOfMonth)
|
||||||
|
}
|
||||||
|
|
||||||
|
static Yearly(count: number, time: Time, dayOfYear: DayOfYear): Frequency {
|
||||||
|
return new Frequency(FrequencyUnit.YEARLY, count, time, dayOfYear)
|
||||||
|
}
|
||||||
|
|
||||||
|
static parse(s: string): Frequency {
|
||||||
|
const parts = s.split(';');
|
||||||
|
let count: number, time: Time;
|
||||||
|
switch (parts[0]) {
|
||||||
|
case 'D':
|
||||||
|
count = Number.parseInt(parts[1]);
|
||||||
|
time = Time.parse(parts[2]);
|
||||||
|
return this.Daily(count, time);
|
||||||
|
case 'W':
|
||||||
|
count = Number.parseInt(parts[1]);
|
||||||
|
time = Time.parse(parts[3]);
|
||||||
|
const daysOfWeek = new Set(parts[2].split(',').map(day => DayOfWeek[day]));
|
||||||
|
return this.Weekly(count, time, daysOfWeek);
|
||||||
|
case 'M':
|
||||||
|
count = Number.parseInt(parts[1]);
|
||||||
|
time = Time.parse(parts[3]);
|
||||||
|
const dayOfMonth = DayOfMonth.parse(parts[2]);
|
||||||
|
return this.Monthly(count, time, dayOfMonth);
|
||||||
|
case 'Y':
|
||||||
|
count = Number.parseInt(parts[1]);
|
||||||
|
time = Time.parse(parts[3]);
|
||||||
|
const dayOfYear = DayOfYear.parse(parts[2]);
|
||||||
|
return this.Yearly(count, time, dayOfYear);
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid Frequency format: ${s}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
let parts = [this.unit.toString()]
|
||||||
|
parts.push(this.count.toString())
|
||||||
|
if (this.amount) {
|
||||||
|
if (this.unit === FrequencyUnit.WEEKLY) {
|
||||||
|
parts.push(Array.from(this.amount as Set<DayOfWeek>).join(','))
|
||||||
|
} else {
|
||||||
|
parts.push(this.amount.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.push(this.time.toString())
|
||||||
|
return parts.join(';')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FrequencyUnit {
|
||||||
|
DAILY = 'D',
|
||||||
|
WEEKLY = 'W',
|
||||||
|
MONTHLY = 'M',
|
||||||
|
YEARLY = 'Y',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Time {
|
||||||
|
hours: number;
|
||||||
|
minutes: number;
|
||||||
|
seconds: number;
|
||||||
|
|
||||||
|
constructor(hours: number, minutes: number, seconds: number) {
|
||||||
|
this.hours = hours;
|
||||||
|
this.minutes = minutes;
|
||||||
|
this.seconds = seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return [
|
||||||
|
String(this.hours).padStart(2, '0'),
|
||||||
|
String(this.minutes).padStart(2, '0'),
|
||||||
|
String(this.seconds).padStart(2, '0'),
|
||||||
|
].join(':')
|
||||||
|
}
|
||||||
|
|
||||||
|
static parse(s: string): Time {
|
||||||
|
if (!s.match(/[0-9]{2}:[0-9]{2}:[0-9]{2}/)) {
|
||||||
|
throw new Error('Invalid time format. Time must be formatted as HH:mm:ss');
|
||||||
|
}
|
||||||
|
const parts = s.split(':').map(part => Number.parseInt(part));
|
||||||
|
return new Time(parts[0], parts[1], parts[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Position {
|
||||||
|
DAY = 'DAY',
|
||||||
|
FIRST = 'FIRST',
|
||||||
|
SECOND = 'SECOND',
|
||||||
|
THIRD = 'THIRD',
|
||||||
|
FOURTH = 'FOURTH',
|
||||||
|
LAST = 'LAST',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DayOfWeek {
|
||||||
|
MONDAY = 'MONDAY',
|
||||||
|
TUESDAY = 'TUESDAY',
|
||||||
|
WEDNESDAY = 'WEDNESDAY',
|
||||||
|
THURSDAY = 'THURSDAY',
|
||||||
|
FRIDAY = 'FRIDAY',
|
||||||
|
SATURDAY = 'SATURDAY',
|
||||||
|
SUNDAY = 'SUNDAY',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DayOfMonth {
|
||||||
|
position: Position;
|
||||||
|
day: (number | DayOfWeek);
|
||||||
|
|
||||||
|
private constructor(position: Position, day: (number | DayOfWeek)) {
|
||||||
|
this.position = position;
|
||||||
|
this.day = day;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Each(day: number): DayOfMonth {
|
||||||
|
if (day < 1 || day > 31) {
|
||||||
|
throw new Error('Day must be between 1 and 31');
|
||||||
|
}
|
||||||
|
return new DayOfMonth(Position.DAY, day);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PositionalDayOfWeek(position: Position, day: DayOfWeek): DayOfMonth {
|
||||||
|
if (position === Position.DAY) {
|
||||||
|
throw new Error('Use DayOfMonth.Each() to create a monthly recurring transaction on the same calendar day');
|
||||||
|
}
|
||||||
|
return new DayOfMonth(position, day)
|
||||||
|
}
|
||||||
|
|
||||||
|
static parse(s: string): DayOfMonth {
|
||||||
|
const parts = s.split('-');
|
||||||
|
const position = Position[parts[0]];
|
||||||
|
if (position === Position.DAY) {
|
||||||
|
return DayOfMonth.Each(Number.parseInt(parts[1]));
|
||||||
|
} else {
|
||||||
|
return DayOfMonth.PositionalDayOfWeek(position, DayOfWeek[parts[1]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `${this.position}-${this.day}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DayOfYear {
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
|
||||||
|
constructor(month: number, day: number) {
|
||||||
|
this.month = month;
|
||||||
|
this.day = day;
|
||||||
|
}
|
||||||
|
|
||||||
|
static parse(s: string): DayOfYear {
|
||||||
|
if (!s.match(/[0-9]{2}-[0-9]{2}/)) {
|
||||||
|
throw new Error(`Invalid format for DayOfYear: ${s}`)
|
||||||
|
}
|
||||||
|
const parts = s.split('-').map(part => Number.parseInt(part));
|
||||||
|
if (parts[0] < 1 || parts[0] > 12) {
|
||||||
|
throw new Error(`Invalid month for DayOfYear: ${parts[0]}`);
|
||||||
|
}
|
||||||
|
let maxDay: number;
|
||||||
|
switch (parts[0]) {
|
||||||
|
case 2:
|
||||||
|
maxDay = 29;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
case 6:
|
||||||
|
case 9:
|
||||||
|
case 11:
|
||||||
|
maxDay = 30;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
maxDay = 31;
|
||||||
|
}
|
||||||
|
if (parts[1] < 1 || parts[1] > maxDay) {
|
||||||
|
throw new Error(`Invalid day for DayOfYear: ${parts[0]}`);
|
||||||
|
}
|
||||||
|
return new DayOfYear(parts[0], parts[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
const monthString = this.month.toString().padStart(2, '0')
|
||||||
|
const dayString = this.day.toString().padStart(2, '0')
|
||||||
|
return `${monthString}-${dayString}`
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,22 +1,21 @@
|
||||||
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpHeaders } from '@angular/common/http';
|
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthInterceptor implements HttpInterceptor {
|
export class AuthInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cookieService: CookieService
|
private storage: Storage
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
if (!this.cookieService.check('Authorization')) {
|
let token = this.storage.getItem('Authorization')
|
||||||
|
if (!token) {
|
||||||
return next.handle(req);
|
return next.handle(req);
|
||||||
}
|
}
|
||||||
let headers = req.headers;
|
let headers = req.headers;
|
||||||
headers = headers.append('Authorization', `Basic ${this.cookieService.get('Authorization')}`);
|
headers = headers.append('Authorization', `Bearer ${token}`);
|
||||||
this.cookieService.set('Authorization', this.cookieService.get('Authorization'), 14, null, null, true);
|
|
||||||
return next.handle(req.clone({headers: headers}));
|
return next.handle(req.clone({headers: headers}));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,167 +1,221 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
import { User, UserPermission, Permission, AuthToken } from '../users/user';
|
||||||
import { Observable, Subscriber } from 'rxjs';
|
|
||||||
import { User, UserPermission, Permission } from '../users/user';
|
|
||||||
import { TwigsService } from './twigs.service';
|
import { TwigsService } from './twigs.service';
|
||||||
import { Budget } from '../budgets/budget';
|
import { Budget } from '../budgets/budget';
|
||||||
import { Category } from '../categories/category';
|
import { Category } from '../categories/category';
|
||||||
import { Transaction } from '../transactions/transaction';
|
import { Transaction } from '../transactions/transaction';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { map } from 'rxjs/operators';
|
import { Frequency, RecurringTransaction } from '../recurringtransactions/recurringtransaction';
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class TwigsHttpService implements TwigsService {
|
export class TwigsHttpService implements TwigsService {
|
||||||
|
|
||||||
constructor(
|
|
||||||
private http: HttpClient,
|
|
||||||
private cookieService: CookieService
|
|
||||||
) { }
|
|
||||||
|
|
||||||
private options = {
|
|
||||||
withCredentials: true
|
|
||||||
};
|
|
||||||
|
|
||||||
private apiUrl = environment.apiUrl;
|
private apiUrl = environment.apiUrl;
|
||||||
|
|
||||||
// Auth
|
constructor(
|
||||||
login(email: string, password: string): Observable<User> {
|
private storage: Storage
|
||||||
// const params = {
|
) { }
|
||||||
// 'username': email,
|
|
||||||
// 'password': password
|
async login(email: string, password: string): Promise<User> {
|
||||||
// };
|
const url = new URL('/api/users/login', this.apiUrl)
|
||||||
// return this.http.post<User>(this.apiUrl + '/users/login', params, this.options);
|
const auth: AuthToken = await this.request(url, HttpMethod.POST, {
|
||||||
const credentials = btoa(`${email}:${password}`)
|
'username': email,
|
||||||
this.cookieService.set('Authorization', credentials, 14, null, null, true);
|
'password': password
|
||||||
return this.getProfile();
|
});
|
||||||
|
this.storage.setItem('Authorization', auth.token);
|
||||||
|
this.storage.setItem('userId', auth.userId);
|
||||||
|
return await this.getProfile(auth.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
register(username: string, email: string, password: string): Observable<User> {
|
register(username: string, email: string, password: string): Promise<User> {
|
||||||
const params = {
|
const body = {
|
||||||
'username': username,
|
'username': username,
|
||||||
'email': email,
|
'email': email,
|
||||||
'password': password
|
'password': password
|
||||||
};
|
};
|
||||||
return this.http.post<User>(this.apiUrl + '/users', params, this.options);
|
const url = new URL('/api/users/register', this.apiUrl)
|
||||||
|
return this.request<User>(url, HttpMethod.POST, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(): Observable<void> {
|
logout(): Promise<void> {
|
||||||
return this.http.post<void>(this.apiUrl + '/login?logout', this.options);
|
this.storage.removeItem('Authorization');
|
||||||
|
this.storage.removeItem('userId');
|
||||||
|
return Promise.resolve()
|
||||||
|
// TODO: Implement this to revoke the token server-side as well
|
||||||
|
// return this.http.post<void>(this.apiUrl + '/login?logout', this.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Budgets
|
// Budgets
|
||||||
getBudgets(): Observable<Budget[]> {
|
getBudgets(): Promise<Budget[]> {
|
||||||
return this.http.get<Budget[]>(this.apiUrl + '/budgets', this.options);
|
const url = new URL('/api/budgets', this.apiUrl)
|
||||||
|
return this.request(url, HttpMethod.GET)
|
||||||
}
|
}
|
||||||
|
|
||||||
getBudget(id: number): Observable<Budget> {
|
getBudgetBalance(
|
||||||
return this.http.get<Budget>(`${this.apiUrl}/budgets/${id}`, this.options);
|
id: string,
|
||||||
|
from?: Date,
|
||||||
|
to?: Date
|
||||||
|
): Promise<number> {
|
||||||
|
const url = new URL('/api/transactions/sum', this.apiUrl)
|
||||||
|
url.searchParams.set('budgetId', id)
|
||||||
|
if (from) {
|
||||||
|
url.searchParams.set('from', from.toISOString());
|
||||||
|
}
|
||||||
|
if (to) {
|
||||||
|
url.searchParams.set('to', to.toISOString());
|
||||||
|
}
|
||||||
|
return this.request(url, HttpMethod.GET).then((res: any) => res.balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
getBudget(id: string): Promise<Budget> {
|
||||||
|
const url = new URL(`/api/budgets/${id}`, this.apiUrl)
|
||||||
|
return this.request(url, HttpMethod.GET)
|
||||||
}
|
}
|
||||||
|
|
||||||
createBudget(
|
createBudget(
|
||||||
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
users: UserPermission[],
|
users: UserPermission[],
|
||||||
): Observable<Budget> {
|
): Promise<Budget> {
|
||||||
const params = {
|
const url = new URL('/api/budgets', this.apiUrl)
|
||||||
|
const body = {
|
||||||
|
'id': id,
|
||||||
'name': name,
|
'name': name,
|
||||||
'description': description,
|
'description': description,
|
||||||
'users': users.map(user => {
|
'users': users.map(userPermission => {
|
||||||
return {
|
return {
|
||||||
user: user.user,
|
user: userPermission.user,
|
||||||
permission: Permission[user.permission]
|
permission: Permission[userPermission.permission]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
return this.http.post<Budget>(this.apiUrl + '/budgets', params, this.options);
|
return this.request(url, HttpMethod.POST, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBudget(id: number, changes: object): Observable<Budget> {
|
updateBudget(id: string, budget: Budget): Promise<Budget> {
|
||||||
return this.http.put<Budget>(`${this.apiUrl}/budgets/${id}`, changes, this.options);
|
const url = new URL(`/api/budgets/${id}`, this.apiUrl)
|
||||||
|
const body = {
|
||||||
|
'name': budget.name,
|
||||||
|
'description': budget.description,
|
||||||
|
'users': budget.users.map(userPermission => {
|
||||||
|
return {
|
||||||
|
user: userPermission.user,
|
||||||
|
permission: Permission[userPermission.permission]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
return this.request(url, HttpMethod.PUT, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteBudget(id: number): Observable<void> {
|
deleteBudget(id: String): Promise<void> {
|
||||||
return this.http.delete<void>(`${this.apiUrl}/budgets/${id}`, this.options);
|
const url = new URL(`/api/budgets/${id}`, this.apiUrl)
|
||||||
|
return this.request(url, HttpMethod.DELETE)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Categories
|
// Categories
|
||||||
getCategories(budgetId: number, count?: number): Observable<Category[]> {
|
getCategories(budgetId: string, count?: number): Promise<Category[]> {
|
||||||
const params = {
|
const url = new URL(`/api/categories`, this.apiUrl)
|
||||||
params: new HttpParams()
|
url.searchParams.set('budgetIds', budgetId)
|
||||||
.set('budgetIds', `${budgetId}`)
|
url.searchParams.set('archived', 'false')
|
||||||
};
|
return this.request(url, HttpMethod.GET);
|
||||||
return this.http.get<Category[]>(`${this.apiUrl}/categories`, Object.assign(params, this.options));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategory(id: number): Observable<Category> {
|
getCategory(id: string): Promise<Category> {
|
||||||
return this.http.get<Category>(`${this.apiUrl}/categories/${id}`, this.options);
|
const url = new URL(`/api/categories/${id}`, this.apiUrl)
|
||||||
|
return this.request(url, HttpMethod.GET);
|
||||||
}
|
}
|
||||||
|
|
||||||
createCategory(budgetId: number, name: string, amount: number, isExpense: boolean): Observable<Category> {
|
async getCategoryBalance(
|
||||||
const params = {
|
id: string,
|
||||||
|
from?: Date,
|
||||||
|
to?: Date
|
||||||
|
): Promise<number> {
|
||||||
|
const url = new URL(`/api/transactions/sum`, this.apiUrl)
|
||||||
|
url.searchParams.set('categoryId', id)
|
||||||
|
if (from) {
|
||||||
|
url.searchParams.set('from', from.toISOString());
|
||||||
|
}
|
||||||
|
if (to) {
|
||||||
|
url.searchParams.set('to', to.toISOString());
|
||||||
|
}
|
||||||
|
const res: any = await this.request(url, HttpMethod.GET);
|
||||||
|
return res.balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
createCategory(id: string, budgetId: string, name: string, description: string, amount: number, isExpense: boolean): Promise<Category> {
|
||||||
|
const url = new URL(`/api/categories`, this.apiUrl)
|
||||||
|
const body = {
|
||||||
|
'id': id,
|
||||||
'title': name,
|
'title': name,
|
||||||
|
'description': description,
|
||||||
'amount': amount,
|
'amount': amount,
|
||||||
'expense': isExpense,
|
'expense': isExpense,
|
||||||
'budgetId': budgetId
|
'budgetId': budgetId
|
||||||
};
|
};
|
||||||
return this.http.post<Category>(this.apiUrl + '/categories', params, this.options);
|
return this.request(url, HttpMethod.POST, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCategory(budgetId: number, id: number, changes: object): Observable<Category> {
|
updateCategory(id: string, changes: object): Promise<Category> {
|
||||||
return this.http.put<Category>(`${this.apiUrl}/categories/${id}`, changes, this.options);
|
const url = new URL(`/api/categories/${id}`, this.apiUrl)
|
||||||
|
return this.request(url, HttpMethod.PUT, changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCategory(budgetId: number, id: number): Observable<void> {
|
deleteCategory(id: string): Promise<void> {
|
||||||
return this.http.delete<void>(`${this.apiUrl}/categories/${id}`, this.options);
|
const url = new URL(`/api/categories/${id}`, this.apiUrl)
|
||||||
|
return this.request(url, HttpMethod.DELETE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transactions
|
// Transactions
|
||||||
getTransactions(
|
async getTransactions(
|
||||||
budgetId?: number,
|
budgetId?: string,
|
||||||
categoryId?: number,
|
categoryId?: string,
|
||||||
count?: number,
|
count?: number,
|
||||||
from?: Date
|
from?: Date,
|
||||||
): Observable<Transaction[]> {
|
to?: Date
|
||||||
let httpParams = new HttpParams();
|
): Promise<Transaction[]> {
|
||||||
|
const url = new URL(`/api/transactions`, this.apiUrl)
|
||||||
if (budgetId) {
|
if (budgetId) {
|
||||||
httpParams = httpParams.set('budgetIds', `${budgetId}`);
|
url.searchParams.set('budgetIds', budgetId);
|
||||||
}
|
}
|
||||||
if (categoryId) {
|
if (categoryId) {
|
||||||
httpParams = httpParams.set('categoryIds', `${categoryId}`);
|
url.searchParams.set('categoryIds', categoryId);
|
||||||
}
|
}
|
||||||
if (from) {
|
if (from) {
|
||||||
httpParams = httpParams.set('from', from.toISOString());
|
url.searchParams.set('from', from.toISOString());
|
||||||
}
|
}
|
||||||
const params = { params: httpParams };
|
if (to) {
|
||||||
return this.http.get<Transaction[]>(`${this.apiUrl}/transactions`, Object.assign(params, this.options))
|
url.searchParams.set('to', to.toISOString());
|
||||||
.pipe(map(transactions => {
|
}
|
||||||
transactions.forEach(transaction => {
|
const transactions: Transaction[] = await this.request(url, HttpMethod.GET)
|
||||||
transaction.date = new Date(transaction.date);
|
transactions.forEach(transaction => {
|
||||||
});
|
transaction.date = new Date(transaction.date);
|
||||||
return transactions;
|
})
|
||||||
}));
|
return transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
getTransaction(id: number): Observable<Transaction> {
|
async getTransaction(id: string): Promise<Transaction> {
|
||||||
return this.http.get<Transaction>(`${this.apiUrl}/transactions/${id}`, this.options)
|
const url = new URL(`/api/transactions/${id}`, this.apiUrl)
|
||||||
.pipe(map(transaction => {
|
const transaction: Transaction = await this.request(url, HttpMethod.GET)
|
||||||
transaction.date = new Date(transaction.date);
|
transaction.date = new Date(transaction.date)
|
||||||
return transaction;
|
return transaction
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createTransaction(
|
async createTransaction(
|
||||||
budgetId: number,
|
id: string,
|
||||||
|
budgetId: string,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
date: Date,
|
date: Date,
|
||||||
expense: boolean,
|
expense: boolean,
|
||||||
category: number
|
category: string
|
||||||
): Observable<Transaction> {
|
): Promise<Transaction> {
|
||||||
const params = {
|
const url = new URL(`/api/transactions`, this.apiUrl)
|
||||||
|
const body = {
|
||||||
|
'id': id,
|
||||||
'title': name,
|
'title': name,
|
||||||
'description': description,
|
'description': description,
|
||||||
'date': date.toISOString(),
|
'date': date.toISOString(),
|
||||||
|
@ -170,25 +224,147 @@ export class TwigsHttpService implements TwigsService {
|
||||||
'categoryId': category,
|
'categoryId': category,
|
||||||
'budgetId': budgetId
|
'budgetId': budgetId
|
||||||
};
|
};
|
||||||
return this.http.post<Transaction>(this.apiUrl + '/transactions', params, this.options);
|
const transaction: Transaction = await this.request(url, HttpMethod.POST, body)
|
||||||
|
transaction.date = new Date(transaction.date)
|
||||||
|
return transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTransaction(budgetId: number, id: number, changes: object): Observable<Transaction> {
|
async updateTransaction(id: string, transaction: Transaction): Promise<Transaction> {
|
||||||
return this.http.put<Transaction>(`${this.apiUrl}/transactions/${id}`, changes, this.options);
|
const body: any = transaction;
|
||||||
|
body.date = transaction.date.toISOString()
|
||||||
|
const url = new URL(`/api/transactions/${id}`, this.apiUrl)
|
||||||
|
const updatedTransaction: Transaction = await this.request(url, HttpMethod.PUT, body)
|
||||||
|
updatedTransaction.date = new Date(updatedTransaction.date)
|
||||||
|
return updatedTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTransaction(budgetId: number, id: number): Observable<void> {
|
deleteTransaction(id: string): Promise<void> {
|
||||||
return this.http.delete<void>(`${this.apiUrl}/transactions/${id}`, this.options);
|
const url = new URL(`/api/transactions/${id}`, this.apiUrl)
|
||||||
|
return this.request(url, HttpMethod.DELETE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurring Transactions
|
||||||
|
async getRecurringTransactions(
|
||||||
|
budgetId?: string,
|
||||||
|
categoryId?: string,
|
||||||
|
count?: number,
|
||||||
|
from?: Date,
|
||||||
|
to?: Date
|
||||||
|
): Promise<RecurringTransaction[]> {
|
||||||
|
const url = new URL(`/api/recurringtransactions`, this.apiUrl)
|
||||||
|
if (budgetId) {
|
||||||
|
url.searchParams.set('budgetIds', budgetId);
|
||||||
|
}
|
||||||
|
if (categoryId) {
|
||||||
|
url.searchParams.set('categoryIds', categoryId);
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
url.searchParams.set('from', from.toISOString());
|
||||||
|
}
|
||||||
|
if (to) {
|
||||||
|
url.searchParams.set('to', to.toISOString());
|
||||||
|
}
|
||||||
|
const transactions: RecurringTransaction[] = await this.request(url, HttpMethod.GET)
|
||||||
|
transactions.forEach(transaction => {
|
||||||
|
transaction.frequency = Frequency.parse(transaction.frequency as any)
|
||||||
|
})
|
||||||
|
return transactions
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRecurringTransaction(id: string): Promise<RecurringTransaction> {
|
||||||
|
const url = new URL(`/api/recurringtransactions/${id}`, this.apiUrl)
|
||||||
|
const transaction: RecurringTransaction = await this.request(url, HttpMethod.GET)
|
||||||
|
transaction.frequency = Frequency.parse(transaction.frequency as any)
|
||||||
|
return transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRecurringTransaction(
|
||||||
|
id: string,
|
||||||
|
budgetId: string,
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
amount: number,
|
||||||
|
frequency: Frequency,
|
||||||
|
start: Date,
|
||||||
|
expense: boolean,
|
||||||
|
category: string,
|
||||||
|
end?: Date,
|
||||||
|
): Promise<RecurringTransaction> {
|
||||||
|
const url = new URL(`/api/transactions`, this.apiUrl)
|
||||||
|
const body = {
|
||||||
|
'id': id,
|
||||||
|
'title': name,
|
||||||
|
'description': description,
|
||||||
|
'frequency': frequency.toString(),
|
||||||
|
'start': start.toISOString(),
|
||||||
|
'finish': end?.toISOString(),
|
||||||
|
'amount': amount,
|
||||||
|
'expense': expense,
|
||||||
|
'categoryId': category,
|
||||||
|
'budgetId': budgetId
|
||||||
|
};
|
||||||
|
const transaction: RecurringTransaction = await this.request(url, HttpMethod.POST, body)
|
||||||
|
transaction.frequency = Frequency.parse(transaction.frequency as any)
|
||||||
|
return transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRecurringTransaction(id: string, transaction: RecurringTransaction): Promise<RecurringTransaction> {
|
||||||
|
const body: any = transaction;
|
||||||
|
body.frequency = transaction.frequency.toString()
|
||||||
|
const url = new URL(`/api/transactions/${id}`, this.apiUrl)
|
||||||
|
const updatedTransaction: RecurringTransaction = await this.request(url, HttpMethod.PUT, body)
|
||||||
|
updatedTransaction.frequency = Frequency.parse(updatedTransaction.frequency as any)
|
||||||
|
return updatedTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRecurringTransaction(id: string): Promise<void> {
|
||||||
|
const url = new URL(`/api/recurringtransactions/${id}`, this.apiUrl)
|
||||||
|
return this.request(url, HttpMethod.DELETE)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
getProfile(): Observable<User> {
|
getProfile(id: string): Promise<User> {
|
||||||
return this.http.get<User>(`${this.apiUrl}/users/me`, this.options);
|
const url = new URL(`/api/users/${id}`, this.apiUrl)
|
||||||
|
return this.request(url, HttpMethod.GET)
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsersByUsername(username: string): Observable<User[]> {
|
getUsersByUsername(username: string): Promise<User[]> {
|
||||||
return Observable.create(subscriber => {
|
return Promise.reject("Not yet implemented")
|
||||||
subscriber.error("Not yet implemented")
|
}
|
||||||
});
|
|
||||||
|
private async request<T>(url: URL, method: HttpMethod, body?: any): Promise<T> {
|
||||||
|
const headers = {
|
||||||
|
'content-type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = this.storage.getItem('Authorization')
|
||||||
|
if (token) {
|
||||||
|
headers['authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
|
let jsonBody: string;
|
||||||
|
if (body) {
|
||||||
|
jsonBody = JSON.stringify(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: headers,
|
||||||
|
method: method,
|
||||||
|
body: jsonBody
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status === 204) {
|
||||||
|
// No content
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HttpMethod {
|
||||||
|
GET = "GET",
|
||||||
|
POST = "POST",
|
||||||
|
PUT = "PUT",
|
||||||
|
DELETE = "DELETE",
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
|
||||||
import { Observable, Subscriber } from 'rxjs';
|
|
||||||
import { User, UserPermission } from '../users/user';
|
import { User, UserPermission } from '../users/user';
|
||||||
import { TwigsService } from './twigs.service';
|
import { TwigsService } from './twigs.service';
|
||||||
import { Budget } from '../budgets/budget';
|
import { Budget } from '../budgets/budget';
|
||||||
import { Category } from '../categories/category';
|
import { Category } from '../categories/category';
|
||||||
import { Transaction } from '../transactions/transaction';
|
import { Transaction } from '../transactions/transaction';
|
||||||
|
import { randomId } from '../shared/utils';
|
||||||
|
import { Frequency, RecurringTransaction } from '../recurringtransactions/recurringtransaction';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is intended to be a very simple implementation of the TwigsService used for testing out the UI and quickly iterating on it.
|
* This is intended to be a very simple implementation of the TwigsService used for testing out the UI and quickly iterating on it.
|
||||||
|
@ -17,89 +17,83 @@ import { Transaction } from '../transactions/transaction';
|
||||||
export class TwigsLocalService implements TwigsService {
|
export class TwigsLocalService implements TwigsService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
private users: User[] = [new User(1, 'test', 'test@example.com')];
|
private users: User[] = [new User(randomId(), 'test', 'test@example.com')];
|
||||||
private budgets: Budget[] = [];
|
private budgets: Budget[] = [];
|
||||||
private transactions: Transaction[] = [];
|
private transactions: Transaction[] = [];
|
||||||
private categories: Category[] = [];
|
private categories: Category[] = [];
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
login(email: string, password: string): Observable<User> {
|
login(email: string, password: string): Promise<User> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
const filteredUsers = this.users.filter(user => {
|
const filteredUsers = this.users.filter(user => {
|
||||||
return (user.email === email || user.username === email);
|
return (user.email === email || user.username === email);
|
||||||
});
|
});
|
||||||
if (filteredUsers.length !== 0) {
|
if (filteredUsers.length !== 0) {
|
||||||
subscriber.next(filteredUsers[0]);
|
resolve(filteredUsers[0]);
|
||||||
} else {
|
} else {
|
||||||
subscriber.error('No users found');
|
reject('No users found');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
register(username: string, email: string, password: string): Observable<User> {
|
register(username: string, email: string, password: string): Promise<User> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
const user = new User();
|
const user = new User();
|
||||||
user.username = username;
|
user.username = username;
|
||||||
user.email = email;
|
user.email = email;
|
||||||
user.id = this.users.length + 1;
|
user.id = randomId();
|
||||||
this.users.push(user);
|
this.users.push(user);
|
||||||
subscriber.next(user);
|
resolve(user);
|
||||||
subscriber.complete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(): Observable<void> {
|
logout(): Promise<void> {
|
||||||
return Observable.create(subscriber => {
|
return Promise.resolve()
|
||||||
subscriber.complete();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Budgets
|
// Budgets
|
||||||
getBudgets(): Observable<Budget[]> {
|
getBudgets(): Promise<Budget[]> {
|
||||||
return Observable.create(subscriber => {
|
return Promise.resolve(this.budgets)
|
||||||
subscriber.next(this.budgets);
|
|
||||||
subscriber.complete();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getBudget(id: number): Observable<Budget> {
|
getBudgetBalance(id: string, from?: Date, to?: Date): Promise<number> {
|
||||||
return Observable.create(subscriber => {
|
return Promise.resolve(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
getBudget(id: string): Promise<Budget> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
const budget = this.budgets.filter(it => {
|
const budget = this.budgets.filter(it => {
|
||||||
return it.id === id;
|
return it.id === id;
|
||||||
})[0];
|
})[0];
|
||||||
if (budget) {
|
if (budget) {
|
||||||
subscriber.next(budget);
|
resolve(budget);
|
||||||
} else {
|
} else {
|
||||||
subscriber.error('No budget found for given id');
|
reject('No budget found for given id');
|
||||||
}
|
}
|
||||||
subscriber.complete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createBudget(
|
createBudget(
|
||||||
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
users: UserPermission[],
|
users: UserPermission[],
|
||||||
): Observable<Budget> {
|
): Promise<Budget> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
const budget = new Budget();
|
const budget = new Budget();
|
||||||
budget.name = name;
|
budget.name = name;
|
||||||
budget.description = description;
|
budget.description = description;
|
||||||
budget.users = this.users.filter(user => {
|
budget.users = users;
|
||||||
return users.map(userPerm => userPerm.user).indexOf(user.id) > -1;
|
budget.id = id;
|
||||||
});
|
|
||||||
budget.id = this.budgets.length + 1;
|
|
||||||
this.budgets.push(budget);
|
this.budgets.push(budget);
|
||||||
subscriber.next(budget);
|
resolve(budget);
|
||||||
subscriber.complete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBudget(id: number, changes: object): Observable<Budget> {
|
updateBudget(id: string, budget: Budget): Promise<Budget> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
const budget = this.budgets.filter(it => {
|
const budget = this.budgets.filter(it => {
|
||||||
return it.id === id;
|
return it.id === id;
|
||||||
})[0];
|
})[0];
|
||||||
|
@ -107,74 +101,71 @@ export class TwigsLocalService implements TwigsService {
|
||||||
const index = this.budgets.indexOf(budget);
|
const index = this.budgets.indexOf(budget);
|
||||||
this.updateValues(
|
this.updateValues(
|
||||||
budget,
|
budget,
|
||||||
changes,
|
budget,
|
||||||
[
|
[
|
||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
|
'users',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
if (changes['userIds']) {
|
|
||||||
budget.users = this.users.filter(user => {
|
|
||||||
return changes['userIds'].indexOf(user.id) > -1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.budgets[index] = budget;
|
this.budgets[index] = budget;
|
||||||
subscriber.next(budget);
|
resolve(budget);
|
||||||
} else {
|
} else {
|
||||||
subscriber.error('No budget found for given id');
|
reject('No budget found for given id');
|
||||||
}
|
}
|
||||||
subscriber.complete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteBudget(id: number): Observable<void> {
|
deleteBudget(id: string): Promise<void> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
const budget = this.budgets.filter(it => {
|
const budget = this.budgets.filter(it => {
|
||||||
return budget.id === id;
|
return budget.id === id;
|
||||||
})[0];
|
})[0];
|
||||||
if (budget) {
|
if (budget) {
|
||||||
const index = this.budgets.indexOf(budget);
|
const index = this.budgets.indexOf(budget);
|
||||||
delete this.budgets[index];
|
delete this.budgets[index];
|
||||||
subscriber.complete();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
subscriber.error('No budget found for given id');
|
reject('No budget found for given id');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Categories
|
// Categories
|
||||||
getCategories(budgetId: number, count?: number): Observable<Category[]> {
|
getCategories(budgetId: string, count?: number): Promise<Category[]> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
subscriber.next(this.categories.filter(category => {
|
resolve(this.categories.filter(category => {
|
||||||
return category.budgetId === budgetId;
|
return category.budgetId === budgetId;
|
||||||
}));
|
}));
|
||||||
subscriber.complete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategory(id: number): Observable<Category> {
|
getCategory(id: string): Promise<Category> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
subscriber.next(this.findById(this.categories, id));
|
resolve(this.findById(this.categories, id));
|
||||||
subscriber.complete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createCategory(budgetId: number, name: string, amount: number, isExpense: boolean): Observable<Category> {
|
getCategoryBalance(id: string, from?: Date, to?: Date): Promise<number> {
|
||||||
return Observable.create(subscriber => {
|
return Promise.resolve(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
createCategory(id: string, budgetId: string, name: string, description: string, amount: number, isExpense: boolean): Promise<Category> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
const category = new Category();
|
const category = new Category();
|
||||||
category.title = name;
|
category.title = name;
|
||||||
|
category.description = description;
|
||||||
category.amount = amount;
|
category.amount = amount;
|
||||||
category.expense = isExpense;
|
category.expense = isExpense;
|
||||||
category.budgetId = budgetId;
|
category.budgetId = budgetId;
|
||||||
category.id = this.categories.length + 1;
|
category.id = id;
|
||||||
this.categories.push(category);
|
this.categories.push(category);
|
||||||
subscriber.next(category);
|
resolve(category);
|
||||||
subscriber.complete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCategory(budgetId: number, id: number, changes: object): Observable<Category> {
|
updateCategory(id: string, changes: object): Promise<Category> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
const category = this.findById(this.categories, id);
|
const category = this.findById(this.categories, id);
|
||||||
if (category) {
|
if (category) {
|
||||||
const index = this.categories.indexOf(category);
|
const index = this.categories.indexOf(category);
|
||||||
|
@ -189,31 +180,30 @@ export class TwigsLocalService implements TwigsService {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
this.categories[index] = category;
|
this.categories[index] = category;
|
||||||
subscriber.next(category);
|
resolve(category);
|
||||||
} else {
|
} else {
|
||||||
subscriber.error('No category found for given id');
|
reject('No category found for given id');
|
||||||
}
|
}
|
||||||
subscriber.complete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCategory(budgetId: number, id: number): Observable<void> {
|
deleteCategory(id: string): Promise<void> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
const category = this.findById(this.categories, id);
|
const category = this.findById(this.categories, id);
|
||||||
if (category) {
|
if (category) {
|
||||||
const index = this.categories.indexOf(category);
|
const index = this.categories.indexOf(category);
|
||||||
delete this.transactions[index];
|
delete this.transactions[index];
|
||||||
subscriber.complete();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
subscriber.error('No category found for given id');
|
reject('No category found for given id');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transactions
|
// Transactions
|
||||||
getTransactions(budgetId?: number, categoryId?: number, count?: number): Observable<Transaction[]> {
|
getTransactions(budgetId?: string, categoryId?: string, count?: number): Promise<Transaction[]> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
subscriber.next(this.transactions.filter(transaction => {
|
resolve(this.transactions.filter(transaction => {
|
||||||
let include = true;
|
let include = true;
|
||||||
if (budgetId) {
|
if (budgetId) {
|
||||||
include = transaction.budgetId === budgetId;
|
include = transaction.budgetId === budgetId;
|
||||||
|
@ -223,27 +213,24 @@ export class TwigsLocalService implements TwigsService {
|
||||||
}
|
}
|
||||||
return include;
|
return include;
|
||||||
}));
|
}));
|
||||||
subscriber.complete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTransaction(id: number): Observable<Transaction> {
|
getTransaction(id: string): Promise<Transaction> {
|
||||||
return Observable.create(subscriber => {
|
return Promise.resolve(this.findById(this.transactions, id));
|
||||||
subscriber.next(this.findById(this.transactions, id));
|
|
||||||
subscriber.complete();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createTransaction(
|
createTransaction(
|
||||||
budgetId: number,
|
id: string,
|
||||||
|
budgetId: string,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
date: Date,
|
date: Date,
|
||||||
isExpense: boolean,
|
isExpense: boolean,
|
||||||
category: number
|
category: string
|
||||||
): Observable<Transaction> {
|
): Promise<Transaction> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = new Transaction();
|
const transaction = new Transaction();
|
||||||
transaction.title = name;
|
transaction.title = name;
|
||||||
transaction.description = description;
|
transaction.description = description;
|
||||||
|
@ -252,15 +239,14 @@ export class TwigsLocalService implements TwigsService {
|
||||||
transaction.expense = isExpense;
|
transaction.expense = isExpense;
|
||||||
transaction.categoryId = category;
|
transaction.categoryId = category;
|
||||||
transaction.budgetId = budgetId;
|
transaction.budgetId = budgetId;
|
||||||
transaction.id = this.transactions.length + 1;
|
transaction.id = randomId();
|
||||||
this.transactions.push(transaction);
|
this.transactions.push(transaction);
|
||||||
subscriber.next(transaction);
|
resolve(transaction);
|
||||||
subscriber.complete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTransaction(budgetId: number, id: number, changes: object): Observable<Transaction> {
|
updateTransaction(id: string, changes: object): Promise<Transaction> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = this.findById(this.transactions, id);
|
const transaction = this.findById(this.transactions, id);
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
const index = this.transactions.indexOf(transaction);
|
const index = this.transactions.indexOf(transaction);
|
||||||
|
@ -279,38 +265,71 @@ export class TwigsLocalService implements TwigsService {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
this.transactions[index] = transaction;
|
this.transactions[index] = transaction;
|
||||||
subscriber.next(transaction);
|
resolve(transaction);
|
||||||
} else {
|
} else {
|
||||||
subscriber.error('No transaction found for given id');
|
reject('No transaction found for given id');
|
||||||
}
|
}
|
||||||
subscriber.complete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTransaction(budgetId: number, id: number): Observable<void> {
|
deleteTransaction(id: string): Promise<void> {
|
||||||
return Observable.create(subscriber => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = this.findById(this.transactions, id);
|
const transaction = this.findById(this.transactions, id);
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
const index = this.transactions.indexOf(transaction);
|
const index = this.transactions.indexOf(transaction);
|
||||||
delete this.transactions[index];
|
delete this.transactions[index];
|
||||||
subscriber.complete();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
subscriber.error('No transaction found for given id');
|
reject('No transaction found for given id');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Users
|
// Recurring Transactions
|
||||||
getProfile(): Observable<User> {
|
getRecurringTransactions(
|
||||||
return Observable.create(subscriber => {
|
budgetId?: string,
|
||||||
subscriber.error("Not yet implemented")
|
categoryId?: string,
|
||||||
});
|
count?: number,
|
||||||
|
from?: Date,
|
||||||
|
to?: Date
|
||||||
|
): Promise<RecurringTransaction[]> {
|
||||||
|
return Promise.reject("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsersByUsername(username: string): Observable<User[]> {
|
getRecurringTransaction(id: string): Promise<RecurringTransaction> {
|
||||||
return Observable.create(subscriber => {
|
return Promise.reject("Not yet implemented")
|
||||||
subscriber.next(this.users.filter(user => user.username.indexOf(username) > -1 ));
|
}
|
||||||
});
|
|
||||||
|
createRecurringTransaction(
|
||||||
|
id: string,
|
||||||
|
budgetId: string,
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
amount: number,
|
||||||
|
frequency: Frequency,
|
||||||
|
start: Date,
|
||||||
|
expense: boolean,
|
||||||
|
category: string,
|
||||||
|
end?: Date,
|
||||||
|
): Promise<RecurringTransaction> {
|
||||||
|
return Promise.reject("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRecurringTransaction(id: string, transaction: RecurringTransaction): Promise<RecurringTransaction> {
|
||||||
|
return Promise.reject("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRecurringTransaction(id: string): Promise<void> {
|
||||||
|
return Promise.reject("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users
|
||||||
|
getProfile(id: string): Promise<User> {
|
||||||
|
return Promise.reject("Not yet implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsersByUsername(username: string): Promise<User[]> {
|
||||||
|
return Promise.resolve(this.users.filter(user => user.username.indexOf(username) > -1))
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateValues(old: object, changes: object, keys: string[]) {
|
private updateValues(old: object, changes: object, keys: string[]) {
|
||||||
|
@ -321,7 +340,7 @@ export class TwigsLocalService implements TwigsService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private findById<T>(items: T[], id: number): T {
|
private findById<T>(items: T[], id: string): T {
|
||||||
return items.filter(item => {
|
return items.filter(item => {
|
||||||
return item['id'] === id;
|
return item['id'] === id;
|
||||||
})[0];
|
})[0];
|
||||||
|
|
|
@ -1,56 +1,85 @@
|
||||||
import { InjectionToken } from '@angular/core';
|
import { InjectionToken } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { User, UserPermission } from '../users/user';
|
import { User, UserPermission } from '../users/user';
|
||||||
import { Budget } from '../budgets/budget';
|
import { Budget } from '../budgets/budget';
|
||||||
import { Category } from '../categories/category';
|
import { Category } from '../categories/category';
|
||||||
|
import { RecurringTransaction, Frequency } from '../recurringtransactions/recurringtransaction';
|
||||||
import { Transaction } from '../transactions/transaction';
|
import { Transaction } from '../transactions/transaction';
|
||||||
|
|
||||||
export interface TwigsService {
|
export interface TwigsService {
|
||||||
// Auth
|
// Auth
|
||||||
login(email: string, password: string): Observable<User>;
|
login(email: string, password: string): Promise<User>;
|
||||||
register(username: string, email: string, password: string): Observable<User>;
|
register(username: string, email: string, password: string): Promise<User>;
|
||||||
logout(): Observable<void>;
|
logout(): Promise<void>;
|
||||||
|
|
||||||
// Budgets
|
// Budgets
|
||||||
getBudgets(): Observable<Budget[]>;
|
getBudgets(): Promise<Budget[]>;
|
||||||
getBudget(id: number): Observable<Budget>;
|
getBudget(id: string): Promise<Budget>;
|
||||||
|
getBudgetBalance(id: string, from?: Date, to?: Date): Promise<number>;
|
||||||
createBudget(
|
createBudget(
|
||||||
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
users: UserPermission[],
|
users: UserPermission[],
|
||||||
): Observable<Budget>;
|
): Promise<Budget>;
|
||||||
updateBudget(id: number, changes: object): Observable<Budget>;
|
updateBudget(id: string, budget: Budget): Promise<Budget>;
|
||||||
deleteBudget(id: number): Observable<void>;
|
deleteBudget(id: string): Promise<void>;
|
||||||
|
|
||||||
// Categories
|
// Categories
|
||||||
getCategories(budgetId?: number, count?: number): Observable<Category[]>;
|
getCategories(budgetId?: string, count?: number): Promise<Category[]>;
|
||||||
getCategory(id: number): Observable<Category>;
|
getCategory(id: string): Promise<Category>;
|
||||||
createCategory(budgetId: number, name: string, amount: number, isExpense: boolean): Observable<Category>;
|
getCategoryBalance(id: string, from?: Date, to?: Date): Promise<number>;
|
||||||
updateCategory(budgetId: number, id: number, changes: object): Observable<Category>;
|
createCategory(id: string, budgetId: string, name: string, description: string, amount: number, isExpense: boolean): Promise<Category>;
|
||||||
deleteCategory(budgetId: number, id: number): Observable<void>;
|
updateCategory(id: string, category: Category): Promise<Category>;
|
||||||
|
deleteCategory(id: string): Promise<void>;
|
||||||
|
|
||||||
// Transactions
|
// Transactions
|
||||||
getTransactions(
|
getTransactions(
|
||||||
budgetId?: number,
|
budgetId?: string,
|
||||||
categoryId?: number,
|
categoryId?: string,
|
||||||
count?: number,
|
count?: number,
|
||||||
from?: Date
|
from?: Date,
|
||||||
): Observable<Transaction[]>;
|
to?: Date
|
||||||
getTransaction(id: number): Observable<Transaction>;
|
): Promise<Transaction[]>;
|
||||||
|
getTransaction(id: string): Promise<Transaction>;
|
||||||
createTransaction(
|
createTransaction(
|
||||||
budgetId: number,
|
id: string,
|
||||||
|
budgetId: string,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
date: Date,
|
date: Date,
|
||||||
isExpense: boolean,
|
isExpense: boolean,
|
||||||
category: number
|
category: string
|
||||||
): Observable<Transaction>;
|
): Promise<Transaction>;
|
||||||
updateTransaction(budgetId: number, id: number, changes: object): Observable<Transaction>;
|
updateTransaction(id: string, transaction: Transaction): Promise<Transaction>;
|
||||||
deleteTransaction(budgetId: number, id: number): Observable<void>;
|
deleteTransaction(id: string): Promise<void>;
|
||||||
|
|
||||||
getProfile(): Observable<User>;
|
// Recurring Transactions
|
||||||
getUsersByUsername(username: string): Observable<User[]>;
|
getRecurringTransactions(
|
||||||
|
budgetId?: string,
|
||||||
|
categoryId?: string,
|
||||||
|
count?: number,
|
||||||
|
from?: Date,
|
||||||
|
to?: Date
|
||||||
|
): Promise<RecurringTransaction[]>;
|
||||||
|
getRecurringTransaction(id: string): Promise<RecurringTransaction>;
|
||||||
|
createRecurringTransaction(
|
||||||
|
id: string,
|
||||||
|
budgetId: string,
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
amount: number,
|
||||||
|
frequency: Frequency,
|
||||||
|
start: Date,
|
||||||
|
expense: boolean,
|
||||||
|
category: string,
|
||||||
|
end?: Date,
|
||||||
|
): Promise<RecurringTransaction>;
|
||||||
|
updateRecurringTransaction(id: string, transaction: RecurringTransaction): Promise<RecurringTransaction>;
|
||||||
|
deleteRecurringTransaction(id: string): Promise<void>;
|
||||||
|
|
||||||
|
getProfile(id: string): Promise<User>;
|
||||||
|
getUsersByUsername(username: string): Promise<User[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export let TWIGS_SERVICE = new InjectionToken<TwigsService>('twigs.service');
|
export let TWIGS_SERVICE = new InjectionToken<TwigsService>('twigs.service');
|
||||||
|
|
15
src/app/shared/utils.ts
Normal file
15
src/app/shared/utils.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
const CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
|
||||||
|
export function randomId(): string {
|
||||||
|
var bytes = new Uint8Array(32)
|
||||||
|
window.crypto.getRandomValues(bytes)
|
||||||
|
return Array.from(bytes, (byte) => CHARACTERS[byte % CHARACTERS.length]).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decimalToInteger(amount: string): number {
|
||||||
|
if (amount[amount.length - 3] === "." || amount[amount.length - 3] === ",") {
|
||||||
|
return Number(amount.replace(/[,.]/g, ""))
|
||||||
|
} else {
|
||||||
|
return Number(amount + "00")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
.transaction-form {
|
.transaction-form {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
color: #F1F1F1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.transaction-form * {
|
.transaction-form * {
|
||||||
|
@ -10,8 +9,3 @@
|
||||||
mat-radio-button {
|
mat-radio-button {
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,16 +3,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="!currentTransaction" *ngIf="currentTransaction" class="form transaction-form">
|
<div [hidden]="!currentTransaction" *ngIf="currentTransaction" class="form transaction-form">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input matInput [(ngModel)]="currentTransaction.title" placeholder="Name" required>
|
<input matInput [(ngModel)]="currentTransaction.title" placeholder="Name" required autocapitalize="words">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<textarea matInput [(ngModel)]="currentTransaction.description" placeholder="Description"></textarea>
|
<textarea matInput [(ngModel)]="currentTransaction.description" placeholder="Description" autocapitalize="sentences"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input matInput type="text" [(ngModel)]="currentTransaction.amount" placeholder="Amount" required currencyMask>
|
<input matInput type="number" [(ngModel)]="currentTransaction.amount" placeholder="Amount" required step="0.01">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input matInput type="date" [ngModel]="transactionDate | date:'yyyy-MM-dd'" (ngModelChange)="transactionDate = $event" placeholder="Date" required>
|
<input matInput type="date" [ngModel]="transactionDate | date:'yyyy-MM-dd'"
|
||||||
|
(ngModelChange)="transactionDate = $event" placeholder="Date" required>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input matInput type="time" [(ngModel)]="currentTime" placeholder="Time" required>
|
<input matInput type="time" [(ngModel)]="currentTime" placeholder="Time" required>
|
||||||
|
@ -29,5 +30,5 @@
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button mat-raised-button color="accent" (click)="save()">Save</button>
|
<button mat-raised-button color="accent" (click)="save()">Save</button>
|
||||||
<button class="button-delete" mat-raised-button color="warn" *ngIf="currentTransaction.id" (click)="delete()">Delete</button>
|
<button class="button-delete" mat-raised-button color="warn" *ngIf="!create" (click)="delete()">Delete</button>
|
||||||
</div>
|
</div>
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { AddEditTransactionComponent } from './add-edit-transaction.component';
|
import { AddEditTransactionComponent } from './add-edit-transaction.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('AddEditTransactionComponent', () => {
|
||||||
let component: AddEditTransactionComponent;
|
let component: AddEditTransactionComponent;
|
||||||
let fixture: ComponentFixture<AddEditTransactionComponent>;
|
let fixture: ComponentFixture<AddEditTransactionComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ AddEditTransactionComponent ]
|
declarations: [ AddEditTransactionComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,7 +4,8 @@ import { TransactionType } from '../transaction.type';
|
||||||
import { Category } from 'src/app/categories/category';
|
import { Category } from 'src/app/categories/category';
|
||||||
import { AppComponent } from 'src/app/app.component';
|
import { AppComponent } from 'src/app/app.component';
|
||||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||||
import { MatRadioChange } from '@angular/material/radio';
|
import { MatLegacyRadioChange as MatRadioChange } from '@angular/material/legacy-radio';
|
||||||
|
import { decimalToInteger } from 'src/app/shared/utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-add-edit-transaction',
|
selector: 'app-add-edit-transaction',
|
||||||
|
@ -14,10 +15,10 @@ import { MatRadioChange } from '@angular/material/radio';
|
||||||
export class AddEditTransactionComponent implements OnInit, OnChanges {
|
export class AddEditTransactionComponent implements OnInit, OnChanges {
|
||||||
@Input() title: string;
|
@Input() title: string;
|
||||||
@Input() currentTransaction: Transaction;
|
@Input() currentTransaction: Transaction;
|
||||||
@Input() budgetId: number;
|
@Input() budgetId: string;
|
||||||
|
@Input() create: boolean
|
||||||
public transactionType = TransactionType;
|
public transactionType = TransactionType;
|
||||||
public categories: Category[];
|
public categories: Category[];
|
||||||
public rawAmount: string;
|
|
||||||
public currentTime: string;
|
public currentTime: string;
|
||||||
public transactionDate: string;
|
public transactionDate: string;
|
||||||
|
|
||||||
|
@ -27,8 +28,8 @@ export class AddEditTransactionComponent implements OnInit, OnChanges {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.app.title = this.title;
|
this.app.setTitle(this.title)
|
||||||
this.app.backEnabled = true;
|
this.app.setBackEnabled(true);
|
||||||
let d: Date, expense: boolean;
|
let d: Date, expense: boolean;
|
||||||
if (this.currentTransaction) {
|
if (this.currentTransaction) {
|
||||||
d = new Date(this.currentTransaction.date);
|
d = new Date(this.currentTransaction.date);
|
||||||
|
@ -48,21 +49,20 @@ export class AddEditTransactionComponent implements OnInit, OnChanges {
|
||||||
}
|
}
|
||||||
|
|
||||||
const d = new Date(changes.currentTransaction.currentValue.date * 1000);
|
const d = new Date(changes.currentTransaction.currentValue.date * 1000);
|
||||||
this.transactionDate = d.toLocaleDateString(undefined, {year: 'numeric', month: '2-digit', day: '2-digit'});
|
this.transactionDate = d.toLocaleDateString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||||
this.currentTime = d.toLocaleTimeString(undefined, {hour: '2-digit', hour12: false, minute: '2-digit'});
|
this.currentTime = d.toLocaleTimeString(undefined, { hour: '2-digit', hour12: false, minute: '2-digit' });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCategories(change: MatRadioChange) {
|
updateCategories(change: MatRadioChange) {
|
||||||
this.twigsService.getCategories(this.budgetId)
|
this.twigsService.getCategories(this.budgetId)
|
||||||
.subscribe(newCategories => {
|
.then(newCategories => {
|
||||||
this.categories = newCategories.filter(category => category.expense === change.value)
|
this.categories = newCategories.filter(category => category.expense === change.value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
// The amount will be input as a decimal value so we need to convert it
|
let promise;
|
||||||
// to an integer
|
this.currentTransaction.amount = decimalToInteger(String(this.currentTransaction.amount))
|
||||||
let observable;
|
|
||||||
this.currentTransaction.date = new Date();
|
this.currentTransaction.date = new Date();
|
||||||
const dateParts = this.transactionDate.split('-');
|
const dateParts = this.transactionDate.split('-');
|
||||||
this.currentTransaction.date.setFullYear(parseInt(dateParts[0], 10));
|
this.currentTransaction.date.setFullYear(parseInt(dateParts[0], 10));
|
||||||
|
@ -71,40 +71,36 @@ export class AddEditTransactionComponent implements OnInit, OnChanges {
|
||||||
const timeParts = this.currentTime.split(':');
|
const timeParts = this.currentTime.split(':');
|
||||||
this.currentTransaction.date.setHours(parseInt(timeParts[0], 10));
|
this.currentTransaction.date.setHours(parseInt(timeParts[0], 10));
|
||||||
this.currentTransaction.date.setMinutes(parseInt(timeParts[1], 10));
|
this.currentTransaction.date.setMinutes(parseInt(timeParts[1], 10));
|
||||||
if (this.currentTransaction.id) {
|
if (this.create) {
|
||||||
// This is an existing transaction, update it
|
|
||||||
observable = this.twigsService.updateTransaction(
|
|
||||||
this.budgetId,
|
|
||||||
this.currentTransaction.id,
|
|
||||||
{
|
|
||||||
name: this.currentTransaction.title,
|
|
||||||
description: this.currentTransaction.description,
|
|
||||||
amount: this.currentTransaction.amount * 100,
|
|
||||||
date: this.currentTransaction.date,
|
|
||||||
categoryId: this.currentTransaction.categoryId,
|
|
||||||
expense: this.currentTransaction.expense
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// This is a new transaction, save it
|
// This is a new transaction, save it
|
||||||
observable = this.twigsService.createTransaction(
|
promise = this.twigsService.createTransaction(
|
||||||
|
this.currentTransaction.id,
|
||||||
this.budgetId,
|
this.budgetId,
|
||||||
this.currentTransaction.title,
|
this.currentTransaction.title,
|
||||||
this.currentTransaction.description,
|
this.currentTransaction.description,
|
||||||
this.currentTransaction.amount * 100,
|
this.currentTransaction.amount,
|
||||||
this.currentTransaction.date,
|
this.currentTransaction.date,
|
||||||
this.currentTransaction.expense,
|
this.currentTransaction.expense,
|
||||||
this.currentTransaction.categoryId,
|
this.currentTransaction.categoryId,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// This is an existing transaction, update it
|
||||||
|
const updatedTransaction: Transaction = {
|
||||||
|
...this.currentTransaction,
|
||||||
|
}
|
||||||
|
promise = this.twigsService.updateTransaction(
|
||||||
|
this.currentTransaction.id,
|
||||||
|
updatedTransaction
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
observable.subscribe(val => {
|
promise.then(() => {
|
||||||
this.app.goBack();
|
this.app.goBack();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(): void {
|
delete(): void {
|
||||||
this.twigsService.deleteTransaction(this.budgetId, this.currentTransaction.id).subscribe(() => {
|
this.twigsService.deleteTransaction(this.currentTransaction.id).then(() => {
|
||||||
this.app.goBack();
|
this.app.goBack();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<app-add-edit-transaction [budgetId]="budgetId" [title]="'Add Transaction'" [currentTransaction]="transaction"></app-add-edit-transaction>
|
<app-add-edit-transaction [budgetId]="budgetId" [title]="'Add Transaction'" [currentTransaction]="transaction" [create]="true"></app-add-edit-transaction>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { NewTransactionComponent } from './new-transaction.component';
|
import { NewTransactionComponent } from './new-transaction.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('NewTransactionComponent', () => {
|
||||||
let component: NewTransactionComponent;
|
let component: NewTransactionComponent;
|
||||||
let fixture: ComponentFixture<NewTransactionComponent>;
|
let fixture: ComponentFixture<NewTransactionComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ NewTransactionComponent ]
|
declarations: [ NewTransactionComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,7 +17,7 @@ export class NewTransactionComponent implements OnInit {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.budgetId = this.route.snapshot.paramMap.get('budgetId');
|
this.budgetId = this.route.snapshot.queryParamMap.get('budgetId');
|
||||||
this.transaction = new Transaction();
|
this.transaction = new Transaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<app-add-edit-transaction [budgetId]="budgetId" [title]="'Edit Transaction'" [currentTransaction]="transaction" *ngIf="transaction"></app-add-edit-transaction>
|
<app-add-edit-transaction [budgetId]="budgetId" [title]="'Edit Transaction'" [currentTransaction]="transaction" *ngIf="transaction" [create]="false"></app-add-edit-transaction>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { TransactionDetailsComponent } from './transaction-details.component';
|
import { TransactionDetailsComponent } from './transaction-details.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('TransactionDetailsComponent', () => {
|
||||||
let component: TransactionDetailsComponent;
|
let component: TransactionDetailsComponent;
|
||||||
let fixture: ComponentFixture<TransactionDetailsComponent>;
|
let fixture: ComponentFixture<TransactionDetailsComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ TransactionDetailsComponent ]
|
declarations: [ TransactionDetailsComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||||
})
|
})
|
||||||
export class TransactionDetailsComponent implements OnInit {
|
export class TransactionDetailsComponent implements OnInit {
|
||||||
|
|
||||||
budgetId: number;
|
budgetId: string;
|
||||||
transaction: Transaction;
|
transaction: Transaction;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -23,9 +23,9 @@ export class TransactionDetailsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
getTransaction(): void {
|
getTransaction(): void {
|
||||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
const id = this.route.snapshot.paramMap.get('id');
|
||||||
this.twigsService.getTransaction(id)
|
this.twigsService.getTransaction(id)
|
||||||
.subscribe(transaction => {
|
.then(transaction => {
|
||||||
transaction.amount /= 100;
|
transaction.amount /= 100;
|
||||||
this.transaction = transaction;
|
this.transaction = transaction;
|
||||||
this.budgetId = transaction.budgetId;
|
this.budgetId = transaction.budgetId;
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
.transactions .list-row-one {
|
.transaction-details p {
|
||||||
display: flex;
|
padding: 2px 0;
|
||||||
justify-content: space-between;
|
}
|
||||||
padding-bottom: 0.2em;
|
|
||||||
|
.transaction-description {
|
||||||
|
font-style: italic;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
<mat-nav-list *ngIf="transactions" class="transactions">
|
<mat-nav-list *ngIf="transactions" class="transactions">
|
||||||
<a mat-list-item *ngFor="let transaction of transactions"
|
<a mat-list-item class="transaction-list-item" *ngFor="let transaction of transactions"
|
||||||
routerLink="/budgets/{{ budgetId }}/transactions/{{ transaction.id }}">
|
routerLink="/transactions/{{ transaction.id }}">
|
||||||
<div matLine class="list-row-one">
|
<div matLine class="transaction-list-details">
|
||||||
<p>{{transaction.title}}</p>
|
<p class="transaction-title">{{transaction.title}}</p>
|
||||||
<p class="amount" [class.expense]="transaction.expense" [class.income]="!transaction.expense">
|
<p class="transaction-description text-small" *ngIf="transaction.description">{{transaction.description }}</p>
|
||||||
{{ transaction.amount / 100 | currency }}
|
<p matLine class="transaction-date text-small">{{ transaction.date | date }}</p>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<p matLine class="text-small">{{ transaction.date | date }}</p>
|
<p class="amount" [class.expense]="transaction.expense" [class.income]="!transaction.expense">
|
||||||
|
{{ transaction.amount / 100 | currency }}
|
||||||
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { TransactionListComponent } from './transaction-list.component';
|
import { TransactionListComponent } from './transaction-list.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('TransactionListComponent', () => {
|
||||||
let component: TransactionListComponent;
|
let component: TransactionListComponent;
|
||||||
let fixture: ComponentFixture<TransactionListComponent>;
|
let fixture: ComponentFixture<TransactionListComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ TransactionListComponent ]
|
declarations: [ TransactionListComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Component, OnInit, Input, Inject } from '@angular/core';
|
import { Component, OnInit, Input, Inject } from '@angular/core';
|
||||||
import { Transaction } from '../transaction';
|
import { Transaction } from '../transaction';
|
||||||
import { TWIGS_SERVICE, TwigsService } from '../../shared/twigs.service';
|
import { TWIGS_SERVICE, TwigsService } from '../../shared/twigs.service';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-transaction-list',
|
selector: 'app-transaction-list',
|
||||||
|
@ -9,12 +10,13 @@ import { TWIGS_SERVICE, TwigsService } from '../../shared/twigs.service';
|
||||||
})
|
})
|
||||||
export class TransactionListComponent implements OnInit {
|
export class TransactionListComponent implements OnInit {
|
||||||
|
|
||||||
@Input() budgetId: number;
|
@Input() budgetIds: string[];
|
||||||
@Input() categoryId?: number;
|
@Input() categoryIds?: string[];
|
||||||
public transactions: Transaction[];
|
public transactions: Transaction[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||||
|
private route: ActivatedRoute
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -22,13 +24,36 @@ export class TransactionListComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
getTransactions(): void {
|
getTransactions(): void {
|
||||||
let date = new Date();
|
let fromStr = this.route.snapshot.queryParamMap.get('from');
|
||||||
date.setHours(0);
|
var from;
|
||||||
date.setMinutes(0);
|
if (fromStr) {
|
||||||
date.setSeconds(0);
|
let fromDate = new Date(fromStr);
|
||||||
date.setMilliseconds(0);
|
if (!isNaN(fromDate.getTime())) {
|
||||||
date.setDate(1);
|
from = fromDate;
|
||||||
this.twigsService.getTransactions(this.budgetId, this.categoryId, null, date).subscribe(transactions => {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!from) {
|
||||||
|
let date = new Date();
|
||||||
|
date.setHours(0);
|
||||||
|
date.setMinutes(0);
|
||||||
|
date.setSeconds(0);
|
||||||
|
date.setMilliseconds(0);
|
||||||
|
date.setDate(1);
|
||||||
|
from = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
let toStr = this.route.snapshot.queryParamMap.get('to');
|
||||||
|
let to: Date;
|
||||||
|
if (toStr) {
|
||||||
|
let toDate = new Date(toStr);
|
||||||
|
if (!isNaN(toDate.getTime())) {
|
||||||
|
to = toDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.twigsService.getTransactions(this.budgetIds.join(','), this.categoryIds?.join(','), null, from, to)
|
||||||
|
.then(transactions => {
|
||||||
this.transactions = transactions;
|
this.transactions = transactions;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
|
import { randomId } from '../shared/utils';
|
||||||
|
|
||||||
export class Transaction {
|
export class Transaction {
|
||||||
id: number;
|
id: string = randomId();
|
||||||
title: string;
|
title: string;
|
||||||
description: string = null;
|
description: string = null;
|
||||||
date: Date = new Date();
|
date: Date = new Date();
|
||||||
amount: number;
|
amount: number;
|
||||||
expense = true;
|
expense = true;
|
||||||
categoryId: number;
|
categoryId: string;
|
||||||
budgetId: number;
|
budgetId: string;
|
||||||
createdBy: number;
|
createdBy: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<app-transaction-list [budgetId]="budgetId" [categoryId]="categoryId"></app-transaction-list>
|
<app-transaction-list [budgetIds]="[budgetId]" [categoryIds]="categoryId"></app-transaction-list>
|
||||||
<a mat-fab routerLink="/budgets/{{ budgetId }}/transactions/new">
|
<a mat-fab routerLink="/transactions/new" [queryParams]="{budgetId: budgetId}">
|
||||||
<mat-icon aria-label="Add">add</mat-icon>
|
<mat-icon aria-label="Add">add</mat-icon>
|
||||||
</a>
|
</a>
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { TransactionsComponent } from './transactions.component';
|
import { TransactionsComponent } from './transactions.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('ExpensesComponent', () => {
|
||||||
let component: TransactionsComponent;
|
let component: TransactionsComponent;
|
||||||
let fixture: ComponentFixture<TransactionsComponent>;
|
let fixture: ComponentFixture<TransactionsComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ TransactionsComponent ]
|
declarations: [ TransactionsComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,8 +9,8 @@ import { ActivatedRoute } from '@angular/router';
|
||||||
})
|
})
|
||||||
export class TransactionsComponent implements OnInit {
|
export class TransactionsComponent implements OnInit {
|
||||||
|
|
||||||
budgetId: number;
|
budgetId?: string;
|
||||||
categoryId?: number;
|
categoryId?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
@ -18,9 +18,9 @@ export class TransactionsComponent implements OnInit {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.budgetId = Number.parseInt(this.route.snapshot.paramMap.get('budgetId'));
|
this.budgetId = this.route.snapshot.queryParamMap.get('budgetIds');
|
||||||
this.categoryId = Number.parseInt(this.route.snapshot.queryParamMap.get('categoryId'));
|
this.categoryId = this.route.snapshot.queryParamMap.get('categoryIds');
|
||||||
this.app.backEnabled = true;
|
this.app.setBackEnabled(true);
|
||||||
this.app.title = 'Transactions';
|
this.app.setTitle('Transactions')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { EditProfileComponent } from './edit-profile.component';
|
import { EditProfileComponent } from './edit-profile.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('EditProfileComponent', () => {
|
||||||
let component: EditProfileComponent;
|
let component: EditProfileComponent;
|
||||||
let fixture: ComponentFixture<EditProfileComponent>;
|
let fixture: ComponentFixture<EditProfileComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ EditProfileComponent ]
|
declarations: [ EditProfileComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { LoginComponent } from './login.component';
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('LoginComponent', () => {
|
||||||
let component: LoginComponent;
|
let component: LoginComponent;
|
||||||
let fixture: ComponentFixture<LoginComponent>;
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ LoginComponent ]
|
declarations: [ LoginComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, Inject, ChangeDetectorRef } from '@angula
|
||||||
import { TwigsService, TWIGS_SERVICE } from '../../shared/twigs.service';
|
import { TwigsService, TWIGS_SERVICE } from '../../shared/twigs.service';
|
||||||
import { User } from '../user';
|
import { User } from '../user';
|
||||||
import { AppComponent } from 'src/app/app.component';
|
import { AppComponent } from 'src/app/app.component';
|
||||||
import { Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
|
@ -14,27 +14,31 @@ export class LoginComponent implements OnInit {
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public email: string;
|
public email: string;
|
||||||
public password: string;
|
public password: string;
|
||||||
|
private redirect: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private app: AppComponent,
|
private app: AppComponent,
|
||||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
private activatedRoute: ActivatedRoute
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.app.title = 'Login';
|
this.app.setTitle('Login')
|
||||||
this.app.backEnabled = true;
|
this.app.setBackEnabled(true);
|
||||||
|
this.redirect = this.activatedRoute.snapshot.queryParamMap.get('redirect');
|
||||||
}
|
}
|
||||||
|
|
||||||
login(): void {
|
login(): void {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.twigsService.login(this.email, this.password)
|
this.twigsService.login(this.email, this.password)
|
||||||
.subscribe(user => {
|
.then(user => {
|
||||||
this.app.user.next(user);
|
this.app.user.next(user);
|
||||||
this.router.navigate(['/'])
|
this.router.navigate([this.redirect || '/'])
|
||||||
},
|
})
|
||||||
error => {
|
.catch(error => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
// TODO: Replace this with an in-app dialog
|
||||||
alert("Login failed. Please verify you have the correct credentials");
|
alert("Login failed. Please verify you have the correct credentials");
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { RegisterComponent } from './register.component';
|
import { RegisterComponent } from './register.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('RegisterComponent', () => {
|
||||||
let component: RegisterComponent;
|
let component: RegisterComponent;
|
||||||
let fixture: ComponentFixture<RegisterComponent>;
|
let fixture: ComponentFixture<RegisterComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ RegisterComponent ]
|
declarations: [ RegisterComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,8 +23,8 @@ export class RegisterComponent implements OnInit {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.app.title = 'Register';
|
this.app.setTitle('Register')
|
||||||
this.app.backEnabled = true;
|
this.app.setBackEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
register(): void {
|
register(): void {
|
||||||
|
@ -33,10 +33,10 @@ export class RegisterComponent implements OnInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.twigsService.register(this.username, this.email, this.password).subscribe(user => {
|
this.twigsService.register(this.username, this.email, this.password).then(user => {
|
||||||
console.log(user);
|
console.log(user);
|
||||||
this.router.navigate(['/'])
|
this.router.navigate(['/'])
|
||||||
}, error => {
|
}).catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert("Registration failed!")
|
alert("Registration failed!")
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { UserComponent } from './user.component';
|
import { UserComponent } from './user.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ describe('UserComponent', () => {
|
||||||
let component: UserComponent;
|
let component: UserComponent;
|
||||||
let fixture: ComponentFixture<UserComponent>;
|
let fixture: ComponentFixture<UserComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ UserComponent ]
|
declarations: [ UserComponent ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,27 +1,36 @@
|
||||||
|
import { randomId } from "../shared/utils";
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
id: number;
|
id: string = randomId();
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
constructor(id?: number, username?: string, email?: string) {
|
constructor(id?: string, username?: string, email?: string) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AuthToken {
|
||||||
|
userId: string;
|
||||||
|
token: string;
|
||||||
|
expiration: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export class UserPermission {
|
export class UserPermission {
|
||||||
user: number;
|
user: string;
|
||||||
permission: Permission;
|
permission: Permission;
|
||||||
|
|
||||||
constructor(user: number, permission: Permission) {
|
constructor(user: string, permission: Permission) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.permission = permission;
|
this.permission = permission;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Permission {
|
export enum Permission {
|
||||||
READ,
|
READ = "READ",
|
||||||
WRITE,
|
WRITE = "WRITE",
|
||||||
OWNER
|
MANAGE = "MANAGE",
|
||||||
|
OWNER = "OWNER"
|
||||||
}
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
export const environment = {
|
|
||||||
production: false,
|
|
||||||
apiUrl: 'https://3000code.brawner.home'
|
|
||||||
};
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
apiUrl: 'https://api.twigs.brawner.dev'
|
apiUrl: 'https://twigs.api.wbrawner.com/api'
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiUrl: 'http://localhost:8080'
|
apiUrl: 'http://localhost:8080/api'
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -13,4 +13,4 @@ export const environment = {
|
||||||
* below file. Don't forget to comment it out in production mode
|
* below file. Don't forget to comment it out in production mode
|
||||||
* because it will have a performance impact when errors are thrown
|
* because it will have a performance impact when errors are thrown
|
||||||
*/
|
*/
|
||||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<meta name="apple-mobile-web-app-title" content="Twigs">
|
<meta name="apple-mobile-web-app-title" content="Twigs">
|
||||||
<meta name="application-name" content="Twigs">
|
<meta name="application-name" content="Twigs">
|
||||||
<meta name="msapplication-TileColor" content="#81c784">
|
<meta name="msapplication-TileColor" content="#81c784">
|
||||||
<meta name="theme-color" content="#212121">
|
<meta name="theme-color" content="#FFFFFF">
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -18,41 +18,6 @@
|
||||||
* BROWSER POLYFILLS
|
* BROWSER POLYFILLS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
|
|
||||||
// import 'core-js/es6/symbol';
|
|
||||||
// import 'core-js/es6/object';
|
|
||||||
// import 'core-js/es6/function';
|
|
||||||
// import 'core-js/es6/parse-int';
|
|
||||||
// import 'core-js/es6/parse-float';
|
|
||||||
// import 'core-js/es6/number';
|
|
||||||
// import 'core-js/es6/math';
|
|
||||||
// import 'core-js/es6/string';
|
|
||||||
// import 'core-js/es6/date';
|
|
||||||
// import 'core-js/es6/array';
|
|
||||||
// import 'core-js/es6/regexp';
|
|
||||||
// import 'core-js/es6/map';
|
|
||||||
// import 'core-js/es6/weak-map';
|
|
||||||
// import 'core-js/es6/set';
|
|
||||||
|
|
||||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
|
||||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
|
||||||
|
|
||||||
/** IE10 and IE11 requires the following for the Reflect API. */
|
|
||||||
import 'core-js/es6/reflect';
|
|
||||||
|
|
||||||
|
|
||||||
/** Evergreen browsers require these. **/
|
|
||||||
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
|
|
||||||
import 'core-js/es7/reflect';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Web Animations `@angular/platform-browser/animations`
|
|
||||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
|
||||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
|
||||||
**/
|
|
||||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||||
|
@ -71,7 +36,7 @@ import 'core-js/es7/reflect';
|
||||||
/***************************************************************************************************
|
/***************************************************************************************************
|
||||||
* Zone JS is required by default for Angular itself.
|
* Zone JS is required by default for Angular itself.
|
||||||
*/
|
*/
|
||||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
import 'zone.js'; // Included with Angular CLI.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
background: #333333;
|
background: #F1F1F1;
|
||||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: #F1F1F1;
|
color: #333333;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@ a.mat-fab {
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-small {
|
.text-small {
|
||||||
font-size: 1em;
|
font-size: 90%;
|
||||||
color: #BDBDBD
|
color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-toolbar.mat-toolbar-row,
|
mat-toolbar.mat-toolbar-row,
|
||||||
|
@ -29,21 +29,21 @@ mat-toolbar.mat-toolbar-single-row {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-toolbar > span {
|
mat-toolbar>span {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 33%;
|
width: 33%;
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-toolbar > span:nth-child(1) {
|
mat-toolbar>span:nth-child(1) {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-toolbar > span:nth-child(2) {
|
mat-toolbar>span:nth-child(2) {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-toolbar > span:nth-child(3) {
|
mat-toolbar>span:nth-child(3) {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ mat-sidenav {
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
color: #F1F1F1;
|
color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form .mat-form-field,
|
.form .mat-form-field,
|
||||||
|
@ -116,3 +116,28 @@ mat-sidenav {
|
||||||
.form mat-radio-button {
|
.form mat-radio-button {
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form button {
|
||||||
|
width: 100%;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #F1F1F1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-small {
|
||||||
|
color: #BDBDBD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
color: #F1F1F1;
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue