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
|
||||
|
||||
# misc
|
||||
/.angular/cache
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
|
@ -42,3 +43,4 @@ Thumbs.db
|
|||
|
||||
# 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
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Chrome",
|
||||
"request": "launch",
|
||||
"type": "pwa-chrome",
|
||||
"url": "http://localhost:4200",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"name": "ng serve",
|
||||
"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
|
||||
WORKDIR /app
|
||||
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": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"aot": true,
|
||||
"outputPath": "dist/twigs",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
|
@ -32,7 +31,13 @@
|
|||
"src/styles.css",
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
"sourceMap": true,
|
||||
"optimization": false,
|
||||
"namedChunks": true
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
@ -51,9 +56,7 @@
|
|||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
|
@ -73,26 +76,27 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": ""
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "twigs:build"
|
||||
"buildTarget": "twigs:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "twigs:build:production"
|
||||
"buildTarget": "twigs:build:production"
|
||||
},
|
||||
"codeserver": {
|
||||
"browserTarget": "twigs:build:codeserver"
|
||||
"buildTarget": "twigs:build:codeserver"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "twigs:build"
|
||||
"buildTarget": "twigs:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
|
@ -112,18 +116,6 @@
|
|||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": "e2e/tsconfig.e2e.json",
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "twigs",
|
||||
"cli": {
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
38859
package-lock.json
generated
38859
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",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "node_modules/@angular/cli/bin/ng",
|
||||
"start": "node_modules/@angular/cli/bin/ng serve --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",
|
||||
"build": "node_modules/@angular/cli/bin/ng build",
|
||||
"package": "node_modules/@angular/cli/bin/ng build --prod --service-worker",
|
||||
"publish": "node_modules/@angular/cli/bin/ng build --prod --service-worker && firebase deploy",
|
||||
"test": "node_modules/@angular/cli/bin/ng test",
|
||||
"lint": "node_modules/@angular/cli/bin/ng lint",
|
||||
"e2e": "node_modules/@angular/cli/bin/ng e2e"
|
||||
"ng": "ng",
|
||||
"start": "ng serve --configuration=production --host '0.0.0.0'",
|
||||
"code-server": "ng serve --configuration=codeserver --host \"0.0.0.0\" --disable-host-check --poll=2000",
|
||||
"build": "ng build",
|
||||
"package": "ng build --configuration=production --service-worker",
|
||||
"publish": "ng build --configuration=production --service-worker && firebase deploy",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"update": "ncu -u"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^10.0.1",
|
||||
"@angular/cdk": "^10.0.1",
|
||||
"@angular/common": "^10.0.1",
|
||||
"@angular/compiler": "^10.0.1",
|
||||
"@angular/core": "^10.0.1",
|
||||
"@angular/forms": "^10.0.1",
|
||||
"@angular/material": "^10.0.1",
|
||||
"@angular/platform-browser": "^10.0.1",
|
||||
"@angular/platform-browser-dynamic": "^10.0.1",
|
||||
"@angular/router": "^10.0.1",
|
||||
"@angular/service-worker": "^10.0.1",
|
||||
"chart.js": "^2.9.3",
|
||||
"core-js": "^2.6.11",
|
||||
"dexie": "^2.0.4",
|
||||
"firebase": "^7.15.5",
|
||||
"ng2-charts": "^2.3.2",
|
||||
"ng2-currency-mask": "^9.0.2",
|
||||
"ngx-cookie-service": "^2.4.0",
|
||||
"rxjs": "^6.5.4",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.10.3"
|
||||
"@angular/animations": "^17.2.3",
|
||||
"@angular/cdk": "^17.2.1",
|
||||
"@angular/common": "^17.2.3",
|
||||
"@angular/compiler": "^17.2.3",
|
||||
"@angular/core": "^17.2.3",
|
||||
"@angular/forms": "^17.2.3",
|
||||
"@angular/material": "^16.2.0",
|
||||
"@angular/platform-browser": "^17.2.3",
|
||||
"@angular/platform-browser-dynamic": "^17.2.3",
|
||||
"@angular/router": "^17.2.3",
|
||||
"@angular/service-worker": "^17.2.3",
|
||||
"chart.js": "^3.7.0",
|
||||
"core-js": "^3.20.3",
|
||||
"ng2-charts": "^3.0.8",
|
||||
"rxjs": "^7.5.2",
|
||||
"tslib": "^2.3.1",
|
||||
"zone.js": "~0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1000.0",
|
||||
"@angular/cli": "^10.0.0",
|
||||
"@angular/compiler-cli": "^10.0.1",
|
||||
"@angular/language-service": "^10.0.1",
|
||||
"@types/jasmine": "^3.5.2",
|
||||
"@types/jasminewd2": "^2.0.8",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^5.1.2",
|
||||
"eslint": "^6.8.0",
|
||||
"firebase-tools": "^7.12.1",
|
||||
"jasmine-core": "~3.5.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~5.0.0",
|
||||
"@angular-devkit/build-angular": "^17.2.2",
|
||||
"@angular/cli": "^17.2.2",
|
||||
"@angular/compiler-cli": "^17.2.3",
|
||||
"@angular/language-service": "^17.2.3",
|
||||
"@types/jasmine": "~3.10.3",
|
||||
"@types/jasminewd2": "^2.0.10",
|
||||
"@types/node": "^17.0.10",
|
||||
"eslint": "^8.7.0",
|
||||
"jasmine-core": "~4.0.0",
|
||||
"jasmine-spec-reporter": "~7.0.0",
|
||||
"karma": "~6.3.11",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~3.3.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.6.2",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "3.9.5"
|
||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
||||
"karma-jasmine": "~4.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.7.0",
|
||||
"npm-check-updates": "^15.0.1",
|
||||
"protractor": "^7.0.0",
|
||||
"ts-node": "~10.4.0",
|
||||
"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 { BudgetsComponent } from './budgets/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 { EditCategoryComponent } from './categories/edit-category/edit-category.component';
|
||||
|
||||
|
@ -20,18 +21,19 @@ const routes: Routes = [
|
|||
{ path: 'budgets', component: BudgetsComponent },
|
||||
{ path: 'budgets/new', component: NewBudgetComponent },
|
||||
{ path: 'budgets/:id', component: BudgetDetailsComponent },
|
||||
{ path: 'budgets/:budgetId/transactions', component: TransactionsComponent },
|
||||
{ path: 'budgets/:budgetId/transactions/new', component: NewTransactionComponent },
|
||||
{ path: 'budgets/:budgetId/transactions/:id', component: TransactionDetailsComponent },
|
||||
{ path: 'budgets/:budgetId/categories', component: CategoriesComponent },
|
||||
{ path: 'budgets/:budgetId/categories/new', component: NewCategoryComponent },
|
||||
{ path: 'budgets/:budgetId/categories/:id', component: CategoryDetailsComponent },
|
||||
{ path: 'budgets/:budgetId/categories/:id/edit', component: EditCategoryComponent },
|
||||
{ path: 'budgets/:id/edit', component: EditBudgetComponent },
|
||||
{ path: 'transactions', component: TransactionsComponent },
|
||||
{ path: 'transactions/new', component: NewTransactionComponent },
|
||||
{ path: 'transactions/:id', component: TransactionDetailsComponent },
|
||||
{ path: 'categories', component: CategoriesComponent },
|
||||
{ path: 'categories/new', component: NewCategoryComponent },
|
||||
{ path: 'categories/:id', component: CategoryDetailsComponent },
|
||||
{ path: 'categories/:id/edit', component: EditCategoryComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes)
|
||||
RouterModule.forRoot(routes, {})
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
mat-toolbar {
|
||||
background-color: #303030;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -3,12 +3,14 @@
|
|||
</p>
|
||||
<mat-sidenav-container *ngIf="online" class="sidenav-container">
|
||||
<mat-sidenav #sidenav mode="over" closed>
|
||||
<mat-nav-list (click)="sidenav.close()">
|
||||
<a mat-list-item *ngIf="loggedIn" routerLink="">{{ getUsername() }}</a>
|
||||
<a mat-list-item *ngIf="loggedIn" routerLink="/budgets">Budgets</a>
|
||||
<a mat-list-item *ngIf="!loggedIn" routerLink="/login">Login</a>
|
||||
<a mat-list-item *ngIf="!loggedIn" routerLink="/register">Register</a>
|
||||
<a mat-list-item *ngIf="loggedIn" (click)="logout()">Logout</a>
|
||||
<mat-nav-list (click)="sidenav.close()" *ngIf="loggedIn">
|
||||
<a mat-list-item routerLink="">{{ getUsername() }}</a>
|
||||
<a mat-list-item routerLink="/budgets">Budgets</a>
|
||||
<a mat-list-item (click)="logout()">Logout</a>
|
||||
</mat-nav-list>
|
||||
<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-sidenav>
|
||||
<mat-sidenav-content>
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
it('should create the app', async(() => {
|
||||
it('should create the app', waitForAsync(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
it(`should have as title 'budget'`, async(() => {
|
||||
it(`should have as title 'budget'`, waitForAsync(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
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);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.debugElement.nativeElement;
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { Component, Inject, ApplicationRef } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, Inject, ApplicationRef, ChangeDetectorRef, OnInit } from '@angular/core';
|
||||
import { DOCUMENT, Location } from '@angular/common';
|
||||
import { User } from './users/user';
|
||||
import { TWIGS_SERVICE, TwigsService } from './shared/twigs.service';
|
||||
import { CookieService } from 'ngx-cookie-service';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { first, filter, map } from 'rxjs/operators';
|
||||
import { interval, concat, BehaviorSubject } from 'rxjs';
|
||||
|
@ -14,7 +13,7 @@ import { Actionable, isActionable } from './shared/actionable';
|
|||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
export class AppComponent implements OnInit {
|
||||
public title = 'Twigs';
|
||||
public backEnabled = false;
|
||||
public user = new BehaviorSubject<User>(null);
|
||||
|
@ -26,38 +25,58 @@ export class AppComponent {
|
|||
constructor(
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
private location: Location,
|
||||
private cookieService: CookieService,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private appRef: ApplicationRef,
|
||||
private updates: SwUpdate,
|
||||
) {
|
||||
if (this.cookieService.check('Authorization')) {
|
||||
this.twigsService.getProfile().subscribe(user => {
|
||||
this.user.next(user);
|
||||
if (this.activatedRoute.pathFromRoot.length == 0) {
|
||||
this.router.navigateByUrl("/budgets")
|
||||
private changeDetector: ChangeDetectorRef,
|
||||
private storage: Storage,
|
||||
@Inject(DOCUMENT) private document: Document
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const unauthenticatedRoutes = [
|
||||
'',
|
||||
'/',
|
||||
'/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 {
|
||||
this.router.navigateByUrl("/login")
|
||||
} else if (unauthenticatedRoutes.indexOf(this.location.path()) == -1) {
|
||||
this.router.navigateByUrl(`/login?redirect=${this.location.path()}`);
|
||||
}
|
||||
|
||||
updates.available.subscribe(event => {
|
||||
console.log('current version is', event.current);
|
||||
console.log('available version is', event.available);
|
||||
this.updates.versionUpdates.subscribe(
|
||||
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
|
||||
updates.activateUpdate();
|
||||
});
|
||||
updates.activated.subscribe(event => {
|
||||
console.log('old version was', event.previous);
|
||||
console.log('new version is', event.current);
|
||||
});
|
||||
this.updates.activateUpdate();
|
||||
}
|
||||
},
|
||||
err => {
|
||||
|
||||
const appIsStable$ = appRef.isStable.pipe(first(isStable => isStable === true));
|
||||
}
|
||||
);
|
||||
|
||||
const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true));
|
||||
const everySixHours$ = interval(6 * 60 * 60 * 1000);
|
||||
const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
|
||||
everySixHoursOnceAppIsStable$.subscribe(() => updates.checkForUpdate());
|
||||
everySixHoursOnceAppIsStable$.subscribe(() => this.updates.checkForUpdate());
|
||||
this.user.subscribe(
|
||||
user => {
|
||||
if (user) {
|
||||
|
@ -67,6 +86,9 @@ export class AppComponent {
|
|||
}
|
||||
}
|
||||
)
|
||||
const darkMode = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
this.handleDarkModeChanges(darkMode);
|
||||
darkMode.addEventListener('change', (e => this.handleDarkModeChanges(e)))
|
||||
}
|
||||
|
||||
getUsername(): String {
|
||||
|
@ -78,8 +100,35 @@ export class AppComponent {
|
|||
}
|
||||
|
||||
logout(): void {
|
||||
this.twigsService.logout().subscribe(_ => {
|
||||
this.twigsService.logout().then(_ => {
|
||||
this.location.go('/');
|
||||
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 {
|
||||
themeColorValue = '#F1F1F1';
|
||||
}
|
||||
themeColor.content = themeColorValue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,20 +2,20 @@ import { BrowserModule } from '@angular/platform-browser';
|
|||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
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 { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||
import { MatLegacyListModule as MatListModule } from '@angular/material/legacy-list';
|
||||
import { MatLegacyProgressBarModule as MatProgressBarModule } from '@angular/material/legacy-progress-bar';
|
||||
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
|
||||
import { MatLegacyRadioModule as MatRadioModule } from '@angular/material/legacy-radio';
|
||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
|
||||
import { AppComponent } from './app.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 { environment } from 'src/environments/environment';
|
||||
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 { 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 { AuthInterceptor } from './shared/auth.interceptor';
|
||||
import { TwigsHttpService } from './shared/twigs.http.service';
|
||||
import { TwigsLocalService } from './shared/twigs.local.service';
|
||||
import { CookieService } from 'ngx-cookie-service';
|
||||
import { TransactionListComponent } from './transactions/transaction-list/transaction-list.component';
|
||||
import { EditCategoryComponent } from './categories/edit-category/edit-category.component';
|
||||
|
||||
export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
||||
align: 'left',
|
||||
precision: 2,
|
||||
prefix: '',
|
||||
thousands: ',',
|
||||
decimal: '.',
|
||||
suffix: '',
|
||||
allowNegative: false,
|
||||
};
|
||||
import { EditBudgetComponent } from './budgets/edit-budget/edit-budget.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -83,6 +72,7 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
|||
CategoryBreakdownComponent,
|
||||
TransactionListComponent,
|
||||
EditCategoryComponent,
|
||||
EditBudgetComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -103,17 +93,15 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = {
|
|||
FormsModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||
HttpClientModule,
|
||||
CurrencyMaskModule,
|
||||
ChartsModule,
|
||||
NgChartsModule,
|
||||
MatCheckboxModule,
|
||||
MatCardModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig },
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||
{ provide: TWIGS_SERVICE, useClass: TwigsHttpService },
|
||||
{ provide: Storage, useValue: window.localStorage },
|
||||
// { provide: TWIGS_SERVICE, useClass: TwigsLocalService },
|
||||
CookieService
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
</div>
|
||||
<div *ngIf="!isLoading && budget" class="form budget-form">
|
||||
<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>
|
||||
<textarea matInput [(ngModel)]="budget.description" placeholder="Description"></textarea>
|
||||
<textarea matInput [(ngModel)]="budget.description" placeholder="Description" autocapitalize="sentences"></textarea>
|
||||
</mat-form-field>
|
||||
<button mat-button color="accent" (click)="save()">Save</button>
|
||||
<button class="button-delete" mat-button color="warn" *ngIf="budget.id" (click)="delete()">Delete</button>
|
||||
<button mat-raised-button color="accent" (click)="save()">Save</button>
|
||||
<button class="button-delete" mat-raised-button color="warn" *ngIf="!create" (click)="delete()">Delete</button>
|
||||
</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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('AddEditBudgetComponent', () => {
|
|||
let component: AddEditBudgetComponent;
|
||||
let fixture: ComponentFixture<AddEditBudgetComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddEditBudgetComponent ]
|
||||
})
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Budget } from '../budget';
|
|||
import { AppComponent } from 'src/app/app.component';
|
||||
import { User, UserPermission, Permission } from 'src/app/users/user';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-edit-budget',
|
||||
|
@ -12,48 +13,53 @@ import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
|||
export class AddEditBudgetComponent {
|
||||
@Input() title: string;
|
||||
@Input() budget: Budget;
|
||||
@Input() create: boolean;
|
||||
public users: UserPermission[];
|
||||
public searchedUsers: User[] = [];
|
||||
public isLoading = false;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private router: Router,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
) {
|
||||
this.app.title = this.title;
|
||||
this.app.backEnabled = true;
|
||||
this.app.setTitle(this.title)
|
||||
this.app.setBackEnabled(true);
|
||||
this.users = [new UserPermission(this.app.user.value.id, Permission.OWNER)];
|
||||
}
|
||||
|
||||
save(): void {
|
||||
let observable;
|
||||
let promise: Promise<Budget>;
|
||||
this.isLoading = true;
|
||||
if (this.budget.id) {
|
||||
// This is an existing transaction, update it
|
||||
observable = this.twigsService.updateBudget(this.budget.id, this.budget);
|
||||
} else {
|
||||
// This is a new transaction, save it
|
||||
observable = this.twigsService.createBudget(
|
||||
if (this.create) {
|
||||
// This is a new budget, save it
|
||||
promise = this.twigsService.createBudget(
|
||||
this.budget.id,
|
||||
this.budget.name,
|
||||
this.budget.description,
|
||||
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
|
||||
observable.subscribe(val => {
|
||||
promise.then(_ => {
|
||||
this.app.goBack();
|
||||
});
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.isLoading = true;
|
||||
this.twigsService.deleteBudget(this.budget.id);
|
||||
this.app.goBack();
|
||||
this.twigsService.deleteBudget(this.budget.id)
|
||||
.then(() => {
|
||||
this.router.navigateByUrl("/budgets");
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Implement a search box with suggestions to add users
|
||||
searchUsers(username: string) {
|
||||
this.twigsService.getUsersByUsername(username).subscribe(users => {
|
||||
this.twigsService.getUsersByUsername(username).then(users => {
|
||||
this.searchedUsers = users;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
.dashboard {
|
||||
color: #F1F1F1;
|
||||
color: #333333;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 1em;
|
||||
}
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.dashboard > mat-card {
|
||||
background: #212121;
|
||||
.dashboard>mat-card {
|
||||
background: #FFFFFF;
|
||||
display: inline-block;
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
|
@ -15,55 +15,97 @@
|
|||
position: relative;
|
||||
width: 100%;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard .dashboard-primary {
|
||||
.dashboard .dashboard-primary {
|
||||
padding: 5em 1em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-primary > * {
|
||||
.dashboard-primary>* {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard div h2, .dashboard div h3 {
|
||||
.dashboard div h2,
|
||||
.dashboard div h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard p, .dashboard a {
|
||||
color: #F1F1F1;
|
||||
.dashboard p,
|
||||
.dashboard a {
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-primary div {
|
||||
.dashboard-primary div {
|
||||
bottom: 0.5em;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
left: 0.5em;
|
||||
right: 0.5em;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard .no-categories {
|
||||
.dashboard .no-categories {
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard .no-categories a {
|
||||
.dashboard .no-categories a {
|
||||
border-color: #333333;
|
||||
display: inline-block;
|
||||
border: 1px dashed #F1F1F1;
|
||||
border: 1px dashed;
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard .no-categories p {
|
||||
.dashboard .no-categories p {
|
||||
line-height: normal;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
a.view-all {
|
||||
a.view-all {
|
||||
position: absolute;
|
||||
right: 0.5em;
|
||||
top: 0.5em;
|
||||
}
|
||||
|
||||
@media (min-width: 1160px) {
|
||||
mat-card {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.dashboard p,
|
||||
.dashboard a {
|
||||
color: #F1F1F1;
|
||||
}
|
||||
|
||||
.dashboard .no-categories a {
|
||||
border-color: #F1F1F1;
|
||||
}
|
||||
}
|
|
@ -3,19 +3,19 @@
|
|||
<h2 class="balance">
|
||||
Current Balance: <br />
|
||||
<span
|
||||
[ngClass]="{'income': getBalance() > 0, 'expense': getBalance() < 0}">{{ getBalance() / 100 | currency }}</span>
|
||||
[ngClass]="{'income': budgetBalance > 0, 'expense': budgetBalance < 0}">{{ budgetBalance / 100 | currency }}</span>
|
||||
</h2>
|
||||
<app-category-breakdown [barChartLabels]="barChartLabels" [barChartData]="barChartData">
|
||||
</app-category-breakdown>
|
||||
<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>
|
||||
</mat-card>
|
||||
<mat-card class="dashboard-categories" [hidden]="!budget">
|
||||
<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">
|
||||
<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>
|
||||
<p>Add categories to gain more insights into your income.</p>
|
||||
</a>
|
||||
|
@ -27,9 +27,9 @@
|
|||
</mat-card>
|
||||
<mat-card class="dashboard-categories" [hidden]="!budget">
|
||||
<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">
|
||||
<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>
|
||||
<p>Add categories to gain more insights into your expenses.</p>
|
||||
</a>
|
||||
|
@ -40,6 +40,6 @@
|
|||
</div>
|
||||
</mat-card>
|
||||
</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>
|
||||
</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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('BudgetDetailsComponent', () => {
|
|||
let component: BudgetDetailsComponent;
|
||||
let fixture: ComponentFixture<BudgetDetailsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
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 { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { Transaction } from 'src/app/transactions/transaction';
|
||||
import { Category } from 'src/app/categories/category';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Label } from 'ng2-charts';
|
||||
import { ChartDataSets } from 'chart.js';
|
||||
import { ChartDataset } from 'chart.js';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
import { Actionable } from '../../shared/actionable';
|
||||
|
||||
@Component({
|
||||
selector: 'app-budget-details',
|
||||
templateUrl: './budget-details.component.html',
|
||||
styleUrls: ['./budget-details.component.css']
|
||||
})
|
||||
export class BudgetDetailsComponent implements OnInit {
|
||||
|
||||
export class BudgetDetailsComponent implements OnInit, OnDestroy, Actionable {
|
||||
budget: Budget;
|
||||
public budgetBalance: number;
|
||||
public transactions: Transaction[];
|
||||
public expenses: Category[] = [];
|
||||
public income: Category[] = [];
|
||||
categoryBalances: Map<number, number>;
|
||||
categoryBalances: Map<string, number>;
|
||||
expectedIncome = 0;
|
||||
actualIncome = 0;
|
||||
expectedExpenses = 0;
|
||||
actualExpenses = 0;
|
||||
barChartLabels: Label[] = ['Income', 'Expenses'];
|
||||
barChartData: ChartDataSets[] = [
|
||||
barChartLabels: string[] = ['Income', 'Expenses'];
|
||||
barChartData: ChartDataset[] = [
|
||||
{ data: [0, 0], label: 'Expected' },
|
||||
{ data: [0, 0], label: 'Actual' },
|
||||
];
|
||||
from: Date
|
||||
to: Date
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
private route: ActivatedRoute,
|
||||
@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() {
|
||||
this.getBudget();
|
||||
this.app.backEnabled = false;
|
||||
this.app.setBackEnabled(false);
|
||||
this.app.setActionable(this)
|
||||
this.categoryBalances = new Map();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.app.setActionable(null)
|
||||
}
|
||||
|
||||
getBudget() {
|
||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
||||
const id = this.route.snapshot.paramMap.get('id');
|
||||
this.twigsService.getBudget(id)
|
||||
.subscribe(budget => {
|
||||
this.app.title = budget.name;
|
||||
.then(budget => {
|
||||
this.app.setTitle(budget.name)
|
||||
this.budget = budget;
|
||||
this.getBalance();
|
||||
this.getTransactions();
|
||||
|
@ -77,15 +110,12 @@ export class BudgetDetailsComponent implements OnInit {
|
|||
];
|
||||
}
|
||||
|
||||
getBalance(): number {
|
||||
let totalBalance = 0;
|
||||
if (!this.categoryBalances) {
|
||||
return 0;
|
||||
}
|
||||
this.categoryBalances.forEach(balance => {
|
||||
totalBalance += balance;
|
||||
getBalance(): void {
|
||||
const id = this.route.snapshot.paramMap.get('id');
|
||||
this.twigsService.getBudgetBalance(id, this.from, this.to)
|
||||
.then(balance => {
|
||||
this.budgetBalance = balance;
|
||||
});
|
||||
return totalBalance;
|
||||
}
|
||||
|
||||
getTransactions(): void {
|
||||
|
@ -96,14 +126,13 @@ export class BudgetDetailsComponent implements OnInit {
|
|||
date.setMilliseconds(0);
|
||||
date.setDate(1);
|
||||
this.twigsService.getTransactions(this.budget.id, null, 5, date)
|
||||
.subscribe(transactions => this.transactions = <Transaction[]>transactions);
|
||||
.then(transactions => this.transactions = <Transaction[]>transactions);
|
||||
}
|
||||
|
||||
getCategories(): void {
|
||||
this.twigsService.getCategories(this.budget.id).subscribe(categories => {
|
||||
const categoryBalances = new Map<number, number>();
|
||||
async getCategories() {
|
||||
const categories = await this.twigsService.getCategories(this.budget.id)
|
||||
const categoryBalances = new Map<string, number>();
|
||||
let categoryBalancesCount = 0;
|
||||
console.log(categories);
|
||||
for (const category of categories) {
|
||||
if (category.expense) {
|
||||
this.expenses.push(category);
|
||||
|
@ -112,8 +141,8 @@ export class BudgetDetailsComponent implements OnInit {
|
|||
this.income.push(category);
|
||||
this.expectedIncome += category.amount;
|
||||
}
|
||||
this.getCategoryBalance(category.id).subscribe(
|
||||
balance => {
|
||||
try {
|
||||
const balance = await this.twigsService.getCategoryBalance(category.id, this.from, this.to)
|
||||
console.log(balance);
|
||||
if (category.expense) {
|
||||
this.actualExpenses += balance * -1;
|
||||
|
@ -121,42 +150,24 @@ export class BudgetDetailsComponent implements OnInit {
|
|||
this.actualIncome += balance;
|
||||
}
|
||||
categoryBalances.set(category.id, balance);
|
||||
categoryBalancesCount++;
|
||||
},
|
||||
error => { categoryBalancesCount++; },
|
||||
() => {
|
||||
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.
|
||||
if (categoryBalancesCount === categories.length) {
|
||||
this.categoryBalances = categoryBalances;
|
||||
this.updateBarChart();
|
||||
}
|
||||
} finally {
|
||||
categoryBalancesCount++;
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCategoryBalance(category: number): Observable<number> {
|
||||
return Observable.create(subscriber => {
|
||||
let date = new Date();
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
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;
|
||||
doAction(): void {
|
||||
this.router.navigateByUrl(this.router.routerState.snapshot.url + "/edit")
|
||||
}
|
||||
}
|
||||
subscriber.next(balance);
|
||||
subscriber.complete();
|
||||
});
|
||||
});
|
||||
|
||||
getActionLabel(): string {
|
||||
return "Edit";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.dashboard {
|
||||
color: #F1F1F1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('BudgetsComponent', () => {
|
|||
let component: BudgetsComponent;
|
||||
let fixture: ComponentFixture<BudgetsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BudgetsComponent ]
|
||||
})
|
||||
|
|
|
@ -20,27 +20,29 @@ export class BudgetsComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.backEnabled = false;
|
||||
this.app.title = 'Budgets';
|
||||
this.app.setBackEnabled(false);
|
||||
this.app.user.subscribe(
|
||||
user => {
|
||||
if (!user) {
|
||||
this.loading = false;
|
||||
this.loggedIn = false;
|
||||
this.app.setTitle('Welcome')
|
||||
return;
|
||||
}
|
||||
this.app.setTitle('Budgets')
|
||||
this.loggedIn = true;
|
||||
this.loading = true;
|
||||
this.twigsService.getBudgets().subscribe(
|
||||
this.twigsService.getBudgets()
|
||||
.then(
|
||||
budgets => {
|
||||
console.log(budgets)
|
||||
this.budgets = budgets;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
error => {
|
||||
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 {
|
||||
id: number;
|
||||
id: string = randomId();
|
||||
name: 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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('NewBudgetComponent', () => {
|
|||
let component: NewBudgetComponent;
|
||||
let fixture: ComponentFixture<NewBudgetComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('CategoriesComponent', () => {
|
|||
let component: CategoriesComponent;
|
||||
let fixture: ComponentFixture<CategoriesComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
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 { 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 { TWIGS_SERVICE, TwigsService } from '../shared/twigs.service';
|
||||
import { Transaction } from '../transactions/transaction';
|
||||
|
||||
@Component({
|
||||
selector: 'app-categories',
|
||||
|
@ -14,9 +12,9 @@ import { TWIGS_SERVICE, TwigsService } from '../shared/twigs.service';
|
|||
})
|
||||
export class CategoriesComponent implements OnInit {
|
||||
|
||||
budgetId: number;
|
||||
budgetId: string;
|
||||
public categories: Category[];
|
||||
public categoryBalances: Map<number, number>;
|
||||
public categoryBalances: Map<string, number>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
|
@ -25,25 +23,30 @@ export class CategoriesComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.budgetId = Number.parseInt(this.route.snapshot.paramMap.get('budgetId'));
|
||||
this.app.title = 'Categories';
|
||||
this.app.backEnabled = true;
|
||||
this.budgetId = this.route.snapshot.paramMap.get('budgetId');
|
||||
this.app.setTitle('Categories')
|
||||
this.app.setBackEnabled(true);
|
||||
this.getCategories();
|
||||
this.categoryBalances = new Map();
|
||||
}
|
||||
|
||||
getCategories(): void {
|
||||
this.twigsService.getCategories(this.budgetId).subscribe(categories => {
|
||||
this.twigsService.getCategories(this.budgetId).then(categories => {
|
||||
this.categories = 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> {
|
||||
return Observable.create(subscriber => {
|
||||
this.twigsService.getTransactions(this.budgetId, category.id).subscribe(transactions => {
|
||||
getCategoryBalance(category: Category): Promise<number> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let transactions: Transaction[]
|
||||
try {
|
||||
transactions = await this.twigsService.getTransactions(this.budgetId, category.id)
|
||||
} catch(e) {
|
||||
reject(e)
|
||||
}
|
||||
let balance = 0;
|
||||
for (const transaction of transactions) {
|
||||
if (transaction.expense) {
|
||||
|
@ -52,8 +55,7 @@ export class CategoriesComponent implements OnInit {
|
|||
balance += transaction.amount;
|
||||
}
|
||||
}
|
||||
subscriber.next(balance);
|
||||
});
|
||||
resolve(balance);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
[options]="barChartOptions"
|
||||
[labels]="barChartLabels"
|
||||
[legend]="barChartLegend"
|
||||
[chartType]="barChartType">
|
||||
[type]="barChartType">
|
||||
</canvas>
|
||||
</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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('CategoryBreakdownComponent', () => {
|
|||
let component: CategoryBreakdownComponent;
|
||||
let fixture: ComponentFixture<CategoryBreakdownComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CategoryBreakdownComponent ]
|
||||
})
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Component, OnInit, Input, OnChanges, SimpleChanges, SimpleChange, ViewChild } from '@angular/core';
|
||||
import { Category } from '../category';
|
||||
import { CategoriesComponent } from '../categories.component';
|
||||
import { ChartOptions, ChartType, ChartDataSets } from 'chart.js';
|
||||
import { BaseChartDirective, Label } from 'ng2-charts';
|
||||
import { ChartConfiguration, ChartType, ChartDataset } from 'chart.js';
|
||||
import { BaseChartDirective } from 'ng2-charts';
|
||||
|
||||
@Component({
|
||||
selector: 'app-category-breakdown',
|
||||
|
@ -10,22 +10,24 @@ import { BaseChartDirective, Label } from 'ng2-charts';
|
|||
styleUrls: ['./category-breakdown.component.css']
|
||||
})
|
||||
export class CategoryBreakdownComponent implements OnInit, OnChanges {
|
||||
barChartOptions: ChartOptions = {
|
||||
barChartOptions: ChartConfiguration['options'] = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
xAxes: [{
|
||||
x: {
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
// beginAtZero: true
|
||||
}
|
||||
}], yAxes: [{}]
|
||||
},
|
||||
y: {}
|
||||
},
|
||||
indexAxis: 'y'
|
||||
};
|
||||
@Input() barChartLabels: Label[];
|
||||
@Input() barChartData: ChartDataSets[] = [
|
||||
@Input() barChartLabels: string[];
|
||||
@Input() barChartData: ChartDataset[] = [
|
||||
{ data: [0, 0, 0, 0], label: '' },
|
||||
];
|
||||
barChartType: ChartType = 'horizontalBar';
|
||||
barChartType: ChartType = 'bar';
|
||||
barChartLegend = true;
|
||||
@ViewChild(BaseChartDirective) chart: BaseChartDirective;
|
||||
|
||||
|
@ -35,9 +37,9 @@ export class CategoryBreakdownComponent implements OnInit, OnChanges {
|
|||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log(changes);
|
||||
if (changes.barChartLabels) {
|
||||
this.barChartLabels = changes.barChartLabels.currentValue;
|
||||
}
|
||||
// if (changes.barChartLabels) {
|
||||
// this.barChartLabels = changes.barChartLabels.currentValue;
|
||||
// }
|
||||
if (changes.barChartData) {
|
||||
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>
|
||||
<a mat-fab routerLink="/budgets/{{ budgetId }}/transactions/new">
|
||||
<p class="category-description" *ngIf="category && category.description" [innerHtml]="category.description"></p>
|
||||
<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>
|
||||
</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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('CategoryDetailsComponent', () => {
|
|||
let component: CategoryDetailsComponent;
|
||||
let fixture: ComponentFixture<CategoryDetailsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CategoryDetailsComponent ]
|
||||
})
|
||||
|
|
|
@ -13,7 +13,7 @@ import { Actionable } from '../../shared/actionable';
|
|||
})
|
||||
export class CategoryDetailsComponent implements OnInit, OnDestroy, Actionable {
|
||||
|
||||
budgetId: number;
|
||||
budgetId: string;
|
||||
category: Category;
|
||||
public transactions: Transaction[];
|
||||
|
||||
|
@ -33,21 +33,21 @@ export class CategoryDetailsComponent implements OnInit, OnDestroy, Actionable {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.app.backEnabled = true;
|
||||
this.app.actionable = this;
|
||||
this.app.setBackEnabled(true);
|
||||
this.app.setActionable(this)
|
||||
this.getCategory();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.app.actionable = null;
|
||||
this.app.setActionable(null)
|
||||
}
|
||||
|
||||
getCategory(): void {
|
||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
||||
const id = this.route.snapshot.paramMap.get('id');
|
||||
this.twigsService.getCategory(id)
|
||||
.subscribe(category => {
|
||||
.then(category => {
|
||||
category.amount /= 100;
|
||||
this.app.title = category.title;
|
||||
this.app.setTitle(category.title)
|
||||
this.category = category;
|
||||
this.budgetId = category.budgetId;
|
||||
});
|
||||
|
|
|
@ -2,11 +2,14 @@
|
|||
<p>Select a category from the list to view details about it or edit it.</p>
|
||||
</div>
|
||||
<div *ngIf="currentCategory" class="form category-form">
|
||||
<mat-form-field (keyup.enter)="doAction()">
|
||||
<input matInput [(ngModel)]="currentCategory.title" placeholder="Name" required>
|
||||
<mat-form-field (keyup.enter)="save()">
|
||||
<input matInput [(ngModel)]="currentCategory.title" placeholder="Name" required autocapitalize="words">
|
||||
</mat-form-field>
|
||||
<mat-form-field (keyup.enter)="doAction()">
|
||||
<input matInput type="text" [(ngModel)]="currentCategory.amount" placeholder="Amount" required currencyMask>
|
||||
<mat-form-field (keyup.enter)="save()">
|
||||
<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-radio-group [(ngModel)]="currentCategory.expense">
|
||||
<mat-radio-button [value]="true">Expense</mat-radio-button>
|
||||
|
@ -18,6 +21,6 @@
|
|||
<input type="color" matInput [(ngModel)]="currentCategory.color" placeholder="Color">
|
||||
</mat-form-field>
|
||||
-->
|
||||
<button mat-button color="accent" (click)="save()">Save</button>
|
||||
<button class="button-delete" mat-button color="warn" *ngIf="currentCategory.id" (click)="delete()">Delete</button>
|
||||
<button mat-raised-button color="accent" (click)="save()">Save</button>
|
||||
<button class="button-delete" mat-raised-button color="warn" *ngIf="!create" (click)="delete()">Delete</button>
|
||||
</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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('CategoryFormComponent', () => {
|
|||
let component: CategoryFormComponent;
|
||||
let fixture: ComponentFixture<CategoryFormComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
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 { AppComponent } from 'src/app/app.component';
|
||||
import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
||||
import { decimalToInteger } from 'src/app/shared/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-category-form',
|
||||
|
@ -10,9 +11,10 @@ import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
|||
})
|
||||
export class CategoryFormComponent implements OnInit {
|
||||
|
||||
@Input() budgetId: number;
|
||||
@Input() budgetId: string;
|
||||
@Input() title: string;
|
||||
@Input() currentCategory: Category;
|
||||
@Input() create: boolean;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
|
@ -20,40 +22,40 @@ export class CategoryFormComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.backEnabled = true;
|
||||
this.app.title = this.title;
|
||||
this.app.setBackEnabled(true);
|
||||
this.app.setTitle(this.title)
|
||||
}
|
||||
|
||||
save(): void {
|
||||
let observable;
|
||||
if (this.currentCategory.id) {
|
||||
// This is an existing category, update it
|
||||
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 {
|
||||
let promise;
|
||||
this.currentCategory.amount = decimalToInteger(String(this.currentCategory.amount))
|
||||
if (this.create) {
|
||||
// This is a new category, save it
|
||||
observable = this.twigsService.createCategory(
|
||||
promise = this.twigsService.createCategory(
|
||||
this.currentCategory.id,
|
||||
this.budgetId,
|
||||
this.currentCategory.title,
|
||||
this.currentCategory.amount * 100,
|
||||
this.currentCategory.description,
|
||||
this.currentCategory.amount,
|
||||
this.currentCategory.expense
|
||||
);
|
||||
} else {
|
||||
// This is an existing category, update it
|
||||
const updatedCategory: Category = {
|
||||
...this.currentCategory,
|
||||
}
|
||||
observable.subscribe(val => {
|
||||
promise = this.twigsService.updateCategory(
|
||||
this.currentCategory.id,
|
||||
this.currentCategory
|
||||
);
|
||||
}
|
||||
promise.then(_ => {
|
||||
this.app.goBack();
|
||||
});
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.twigsService.deleteCategory(this.budgetId, this.currentCategory.id).subscribe(() => {
|
||||
this.twigsService.deleteCategory(this.currentCategory.id).then(() => {
|
||||
this.app.goBack();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
.categories mat-progress-bar.mat-progress-bar {
|
||||
background-color: #BDBDBD;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
::ng-deep .mat-progress-bar-buffer {
|
||||
background-color: #BDBDBD;
|
||||
}
|
||||
|
||||
p.mat-line.category-list-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -26,5 +21,11 @@ p.mat-line.category-list-title .remaining {
|
|||
}
|
||||
|
||||
::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">
|
||||
<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">
|
||||
<span>
|
||||
{{ 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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('CategoryListComponent', () => {
|
|||
let component: CategoryListComponent;
|
||||
let fixture: ComponentFixture<CategoryListComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CategoryListComponent ]
|
||||
})
|
||||
|
|
|
@ -10,7 +10,7 @@ export class CategoryListComponent implements OnInit {
|
|||
|
||||
@Input() budgetId: string;
|
||||
@Input() categories: Category[];
|
||||
@Input() categoryBalances: Map<number, number>;
|
||||
@Input() categoryBalances: Map<string, number>;
|
||||
|
||||
constructor() { }
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { randomId } from '../shared/utils';
|
||||
|
||||
export class Category {
|
||||
id: number;
|
||||
id: string = randomId();
|
||||
title: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
expense: 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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('EditCategoryComponent', () => {
|
|||
let component: EditCategoryComponent;
|
||||
let fixture: ComponentFixture<EditCategoryComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EditCategoryComponent ]
|
||||
})
|
||||
|
|
|
@ -11,7 +11,7 @@ import { TWIGS_SERVICE, TwigsService } from '../../shared/twigs.service';
|
|||
})
|
||||
export class EditCategoryComponent implements OnInit {
|
||||
|
||||
budgetId: number;
|
||||
budgetId: string;
|
||||
category: Category;
|
||||
|
||||
constructor(
|
||||
|
@ -21,16 +21,16 @@ export class EditCategoryComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.app.backEnabled = true;
|
||||
this.app.setBackEnabled(true);
|
||||
this.getCategory();
|
||||
}
|
||||
|
||||
getCategory(): void {
|
||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
||||
const id = this.route.snapshot.paramMap.get('id');
|
||||
this.twigsService.getCategory(id)
|
||||
.subscribe(category => {
|
||||
.then(category => {
|
||||
category.amount /= 100;
|
||||
this.app.title = category.title;
|
||||
this.app.setTitle(category.title)
|
||||
this.category = category;
|
||||
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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('NewCategoryComponent', () => {
|
|||
let component: NewCategoryComponent;
|
||||
let fixture: ComponentFixture<NewCategoryComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ NewCategoryComponent ]
|
||||
})
|
||||
|
|
|
@ -17,7 +17,8 @@ export class NewCategoryComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
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();
|
||||
// TODO: Set random color for category, improve color picker
|
||||
// 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 { Injectable } from '@angular/core';
|
||||
import { CookieService } from 'ngx-cookie-service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
|
||||
constructor(
|
||||
private cookieService: CookieService
|
||||
private storage: Storage
|
||||
) { }
|
||||
|
||||
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);
|
||||
}
|
||||
let headers = req.headers;
|
||||
headers = headers.append('Authorization', `Basic ${this.cookieService.get('Authorization')}`);
|
||||
this.cookieService.set('Authorization', this.cookieService.get('Authorization'), 14, null, null, true);
|
||||
headers = headers.append('Authorization', `Bearer ${token}`);
|
||||
return next.handle(req.clone({headers: headers}));
|
||||
}
|
||||
}
|
|
@ -1,167 +1,221 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, Subscriber } from 'rxjs';
|
||||
import { User, UserPermission, Permission } from '../users/user';
|
||||
import { User, UserPermission, Permission, AuthToken } from '../users/user';
|
||||
import { TwigsService } from './twigs.service';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { Category } from '../categories/category';
|
||||
import { Transaction } from '../transactions/transaction';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { CookieService } from 'ngx-cookie-service';
|
||||
import { Frequency, RecurringTransaction } from '../recurringtransactions/recurringtransaction';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TwigsHttpService implements TwigsService {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private cookieService: CookieService
|
||||
) { }
|
||||
|
||||
private options = {
|
||||
withCredentials: true
|
||||
};
|
||||
|
||||
private apiUrl = environment.apiUrl;
|
||||
|
||||
// Auth
|
||||
login(email: string, password: string): Observable<User> {
|
||||
// const params = {
|
||||
// 'username': email,
|
||||
// 'password': password
|
||||
// };
|
||||
// return this.http.post<User>(this.apiUrl + '/users/login', params, this.options);
|
||||
const credentials = btoa(`${email}:${password}`)
|
||||
this.cookieService.set('Authorization', credentials, 14, null, null, true);
|
||||
return this.getProfile();
|
||||
constructor(
|
||||
private storage: Storage
|
||||
) { }
|
||||
|
||||
async login(email: string, password: string): Promise<User> {
|
||||
const url = new URL('/api/users/login', this.apiUrl)
|
||||
const auth: AuthToken = await this.request(url, HttpMethod.POST, {
|
||||
'username': email,
|
||||
'password': password
|
||||
});
|
||||
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> {
|
||||
const params = {
|
||||
register(username: string, email: string, password: string): Promise<User> {
|
||||
const body = {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'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> {
|
||||
return this.http.post<void>(this.apiUrl + '/login?logout', this.options);
|
||||
logout(): Promise<void> {
|
||||
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
|
||||
getBudgets(): Observable<Budget[]> {
|
||||
return this.http.get<Budget[]>(this.apiUrl + '/budgets', this.options);
|
||||
getBudgets(): Promise<Budget[]> {
|
||||
const url = new URL('/api/budgets', this.apiUrl)
|
||||
return this.request(url, HttpMethod.GET)
|
||||
}
|
||||
|
||||
getBudget(id: number): Observable<Budget> {
|
||||
return this.http.get<Budget>(`${this.apiUrl}/budgets/${id}`, this.options);
|
||||
getBudgetBalance(
|
||||
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(
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
users: UserPermission[],
|
||||
): Observable<Budget> {
|
||||
const params = {
|
||||
): Promise<Budget> {
|
||||
const url = new URL('/api/budgets', this.apiUrl)
|
||||
const body = {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'users': users.map(user => {
|
||||
'users': users.map(userPermission => {
|
||||
return {
|
||||
user: user.user,
|
||||
permission: Permission[user.permission]
|
||||
user: userPermission.user,
|
||||
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> {
|
||||
return this.http.put<Budget>(`${this.apiUrl}/budgets/${id}`, changes, this.options);
|
||||
updateBudget(id: string, budget: Budget): Promise<Budget> {
|
||||
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> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/budgets/${id}`, this.options);
|
||||
deleteBudget(id: String): Promise<void> {
|
||||
const url = new URL(`/api/budgets/${id}`, this.apiUrl)
|
||||
return this.request(url, HttpMethod.DELETE)
|
||||
}
|
||||
|
||||
// Categories
|
||||
getCategories(budgetId: number, count?: number): Observable<Category[]> {
|
||||
const params = {
|
||||
params: new HttpParams()
|
||||
.set('budgetIds', `${budgetId}`)
|
||||
};
|
||||
return this.http.get<Category[]>(`${this.apiUrl}/categories`, Object.assign(params, this.options));
|
||||
getCategories(budgetId: string, count?: number): Promise<Category[]> {
|
||||
const url = new URL(`/api/categories`, this.apiUrl)
|
||||
url.searchParams.set('budgetIds', budgetId)
|
||||
url.searchParams.set('archived', 'false')
|
||||
return this.request(url, HttpMethod.GET);
|
||||
}
|
||||
|
||||
getCategory(id: number): Observable<Category> {
|
||||
return this.http.get<Category>(`${this.apiUrl}/categories/${id}`, this.options);
|
||||
getCategory(id: string): Promise<Category> {
|
||||
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> {
|
||||
const params = {
|
||||
async getCategoryBalance(
|
||||
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,
|
||||
'description': description,
|
||||
'amount': amount,
|
||||
'expense': isExpense,
|
||||
'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> {
|
||||
return this.http.put<Category>(`${this.apiUrl}/categories/${id}`, changes, this.options);
|
||||
updateCategory(id: string, changes: object): Promise<Category> {
|
||||
const url = new URL(`/api/categories/${id}`, this.apiUrl)
|
||||
return this.request(url, HttpMethod.PUT, changes);
|
||||
}
|
||||
|
||||
deleteCategory(budgetId: number, id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/categories/${id}`, this.options);
|
||||
deleteCategory(id: string): Promise<void> {
|
||||
const url = new URL(`/api/categories/${id}`, this.apiUrl)
|
||||
return this.request(url, HttpMethod.DELETE);
|
||||
}
|
||||
|
||||
// Transactions
|
||||
getTransactions(
|
||||
budgetId?: number,
|
||||
categoryId?: number,
|
||||
async getTransactions(
|
||||
budgetId?: string,
|
||||
categoryId?: string,
|
||||
count?: number,
|
||||
from?: Date
|
||||
): Observable<Transaction[]> {
|
||||
let httpParams = new HttpParams();
|
||||
from?: Date,
|
||||
to?: Date
|
||||
): Promise<Transaction[]> {
|
||||
const url = new URL(`/api/transactions`, this.apiUrl)
|
||||
if (budgetId) {
|
||||
httpParams = httpParams.set('budgetIds', `${budgetId}`);
|
||||
url.searchParams.set('budgetIds', budgetId);
|
||||
}
|
||||
if (categoryId) {
|
||||
httpParams = httpParams.set('categoryIds', `${categoryId}`);
|
||||
url.searchParams.set('categoryIds', categoryId);
|
||||
}
|
||||
if (from) {
|
||||
httpParams = httpParams.set('from', from.toISOString());
|
||||
url.searchParams.set('from', from.toISOString());
|
||||
}
|
||||
const params = { params: httpParams };
|
||||
return this.http.get<Transaction[]>(`${this.apiUrl}/transactions`, Object.assign(params, this.options))
|
||||
.pipe(map(transactions => {
|
||||
if (to) {
|
||||
url.searchParams.set('to', to.toISOString());
|
||||
}
|
||||
const transactions: Transaction[] = await this.request(url, HttpMethod.GET)
|
||||
transactions.forEach(transaction => {
|
||||
transaction.date = new Date(transaction.date);
|
||||
});
|
||||
return transactions;
|
||||
}));
|
||||
})
|
||||
return transactions
|
||||
}
|
||||
|
||||
getTransaction(id: number): Observable<Transaction> {
|
||||
return this.http.get<Transaction>(`${this.apiUrl}/transactions/${id}`, this.options)
|
||||
.pipe(map(transaction => {
|
||||
transaction.date = new Date(transaction.date);
|
||||
return transaction;
|
||||
}));
|
||||
async getTransaction(id: string): Promise<Transaction> {
|
||||
const url = new URL(`/api/transactions/${id}`, this.apiUrl)
|
||||
const transaction: Transaction = await this.request(url, HttpMethod.GET)
|
||||
transaction.date = new Date(transaction.date)
|
||||
return transaction
|
||||
}
|
||||
|
||||
createTransaction(
|
||||
budgetId: number,
|
||||
async createTransaction(
|
||||
id: string,
|
||||
budgetId: string,
|
||||
name: string,
|
||||
description: string,
|
||||
amount: number,
|
||||
date: Date,
|
||||
expense: boolean,
|
||||
category: number
|
||||
): Observable<Transaction> {
|
||||
const params = {
|
||||
category: string
|
||||
): Promise<Transaction> {
|
||||
const url = new URL(`/api/transactions`, this.apiUrl)
|
||||
const body = {
|
||||
'id': id,
|
||||
'title': name,
|
||||
'description': description,
|
||||
'date': date.toISOString(),
|
||||
|
@ -170,25 +224,147 @@ export class TwigsHttpService implements TwigsService {
|
|||
'categoryId': category,
|
||||
'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> {
|
||||
return this.http.put<Transaction>(`${this.apiUrl}/transactions/${id}`, changes, this.options);
|
||||
async updateTransaction(id: string, transaction: Transaction): Promise<Transaction> {
|
||||
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> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/transactions/${id}`, this.options);
|
||||
deleteTransaction(id: string): Promise<void> {
|
||||
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
|
||||
getProfile(): Observable<User> {
|
||||
return this.http.get<User>(`${this.apiUrl}/users/me`, this.options);
|
||||
getProfile(id: string): Promise<User> {
|
||||
const url = new URL(`/api/users/${id}`, this.apiUrl)
|
||||
return this.request(url, HttpMethod.GET)
|
||||
}
|
||||
|
||||
getUsersByUsername(username: string): Observable<User[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.error("Not yet implemented")
|
||||
});
|
||||
getUsersByUsername(username: string): Promise<User[]> {
|
||||
return Promise.reject("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 { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, Subscriber } from 'rxjs';
|
||||
import { User, UserPermission } from '../users/user';
|
||||
import { TwigsService } from './twigs.service';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { Category } from '../categories/category';
|
||||
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.
|
||||
|
@ -17,89 +17,83 @@ import { Transaction } from '../transactions/transaction';
|
|||
export class TwigsLocalService implements TwigsService {
|
||||
|
||||
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 transactions: Transaction[] = [];
|
||||
private categories: Category[] = [];
|
||||
|
||||
// Auth
|
||||
login(email: string, password: string): Observable<User> {
|
||||
return Observable.create(subscriber => {
|
||||
login(email: string, password: string): Promise<User> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const filteredUsers = this.users.filter(user => {
|
||||
return (user.email === email || user.username === email);
|
||||
});
|
||||
if (filteredUsers.length !== 0) {
|
||||
subscriber.next(filteredUsers[0]);
|
||||
resolve(filteredUsers[0]);
|
||||
} else {
|
||||
subscriber.error('No users found');
|
||||
reject('No users found');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
register(username: string, email: string, password: string): Observable<User> {
|
||||
return Observable.create(subscriber => {
|
||||
register(username: string, email: string, password: string): Promise<User> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const user = new User();
|
||||
user.username = username;
|
||||
user.email = email;
|
||||
user.id = this.users.length + 1;
|
||||
user.id = randomId();
|
||||
this.users.push(user);
|
||||
subscriber.next(user);
|
||||
subscriber.complete();
|
||||
resolve(user);
|
||||
});
|
||||
}
|
||||
|
||||
logout(): Observable<void> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.complete();
|
||||
});
|
||||
logout(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// Budgets
|
||||
getBudgets(): Observable<Budget[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.budgets);
|
||||
subscriber.complete();
|
||||
});
|
||||
getBudgets(): Promise<Budget[]> {
|
||||
return Promise.resolve(this.budgets)
|
||||
}
|
||||
|
||||
getBudget(id: number): Observable<Budget> {
|
||||
return Observable.create(subscriber => {
|
||||
getBudgetBalance(id: string, from?: Date, to?: Date): Promise<number> {
|
||||
return Promise.resolve(200)
|
||||
}
|
||||
|
||||
getBudget(id: string): Promise<Budget> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const budget = this.budgets.filter(it => {
|
||||
return it.id === id;
|
||||
})[0];
|
||||
if (budget) {
|
||||
subscriber.next(budget);
|
||||
resolve(budget);
|
||||
} else {
|
||||
subscriber.error('No budget found for given id');
|
||||
reject('No budget found for given id');
|
||||
}
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
createBudget(
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
users: UserPermission[],
|
||||
): Observable<Budget> {
|
||||
return Observable.create(subscriber => {
|
||||
): Promise<Budget> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const budget = new Budget();
|
||||
budget.name = name;
|
||||
budget.description = description;
|
||||
budget.users = this.users.filter(user => {
|
||||
return users.map(userPerm => userPerm.user).indexOf(user.id) > -1;
|
||||
});
|
||||
budget.id = this.budgets.length + 1;
|
||||
budget.users = users;
|
||||
budget.id = id;
|
||||
this.budgets.push(budget);
|
||||
subscriber.next(budget);
|
||||
subscriber.complete();
|
||||
resolve(budget);
|
||||
});
|
||||
}
|
||||
|
||||
updateBudget(id: number, changes: object): Observable<Budget> {
|
||||
return Observable.create(subscriber => {
|
||||
updateBudget(id: string, budget: Budget): Promise<Budget> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const budget = this.budgets.filter(it => {
|
||||
return it.id === id;
|
||||
})[0];
|
||||
|
@ -107,74 +101,71 @@ export class TwigsLocalService implements TwigsService {
|
|||
const index = this.budgets.indexOf(budget);
|
||||
this.updateValues(
|
||||
budget,
|
||||
changes,
|
||||
budget,
|
||||
[
|
||||
'name',
|
||||
'description',
|
||||
'users',
|
||||
]
|
||||
);
|
||||
if (changes['userIds']) {
|
||||
budget.users = this.users.filter(user => {
|
||||
return changes['userIds'].indexOf(user.id) > -1;
|
||||
});
|
||||
}
|
||||
this.budgets[index] = budget;
|
||||
subscriber.next(budget);
|
||||
resolve(budget);
|
||||
} else {
|
||||
subscriber.error('No budget found for given id');
|
||||
reject('No budget found for given id');
|
||||
}
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
deleteBudget(id: number): Observable<void> {
|
||||
return Observable.create(subscriber => {
|
||||
deleteBudget(id: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const budget = this.budgets.filter(it => {
|
||||
return budget.id === id;
|
||||
})[0];
|
||||
if (budget) {
|
||||
const index = this.budgets.indexOf(budget);
|
||||
delete this.budgets[index];
|
||||
subscriber.complete();
|
||||
resolve();
|
||||
} else {
|
||||
subscriber.error('No budget found for given id');
|
||||
reject('No budget found for given id');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Categories
|
||||
getCategories(budgetId: number, count?: number): Observable<Category[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.categories.filter(category => {
|
||||
getCategories(budgetId: string, count?: number): Promise<Category[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this.categories.filter(category => {
|
||||
return category.budgetId === budgetId;
|
||||
}));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
getCategory(id: number): Observable<Category> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.findById(this.categories, id));
|
||||
subscriber.complete();
|
||||
getCategory(id: string): Promise<Category> {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this.findById(this.categories, id));
|
||||
});
|
||||
}
|
||||
|
||||
createCategory(budgetId: number, name: string, amount: number, isExpense: boolean): Observable<Category> {
|
||||
return Observable.create(subscriber => {
|
||||
getCategoryBalance(id: string, from?: Date, to?: Date): Promise<number> {
|
||||
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();
|
||||
category.title = name;
|
||||
category.description = description;
|
||||
category.amount = amount;
|
||||
category.expense = isExpense;
|
||||
category.budgetId = budgetId;
|
||||
category.id = this.categories.length + 1;
|
||||
category.id = id;
|
||||
this.categories.push(category);
|
||||
subscriber.next(category);
|
||||
subscriber.complete();
|
||||
resolve(category);
|
||||
});
|
||||
}
|
||||
|
||||
updateCategory(budgetId: number, id: number, changes: object): Observable<Category> {
|
||||
return Observable.create(subscriber => {
|
||||
updateCategory(id: string, changes: object): Promise<Category> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const category = this.findById(this.categories, id);
|
||||
if (category) {
|
||||
const index = this.categories.indexOf(category);
|
||||
|
@ -189,31 +180,30 @@ export class TwigsLocalService implements TwigsService {
|
|||
]
|
||||
);
|
||||
this.categories[index] = category;
|
||||
subscriber.next(category);
|
||||
resolve(category);
|
||||
} 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> {
|
||||
return Observable.create(subscriber => {
|
||||
deleteCategory(id: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const category = this.findById(this.categories, id);
|
||||
if (category) {
|
||||
const index = this.categories.indexOf(category);
|
||||
delete this.transactions[index];
|
||||
subscriber.complete();
|
||||
resolve();
|
||||
} else {
|
||||
subscriber.error('No category found for given id');
|
||||
reject('No category found for given id');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Transactions
|
||||
getTransactions(budgetId?: number, categoryId?: number, count?: number): Observable<Transaction[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.transactions.filter(transaction => {
|
||||
getTransactions(budgetId?: string, categoryId?: string, count?: number): Promise<Transaction[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this.transactions.filter(transaction => {
|
||||
let include = true;
|
||||
if (budgetId) {
|
||||
include = transaction.budgetId === budgetId;
|
||||
|
@ -223,27 +213,24 @@ export class TwigsLocalService implements TwigsService {
|
|||
}
|
||||
return include;
|
||||
}));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
getTransaction(id: number): Observable<Transaction> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.findById(this.transactions, id));
|
||||
subscriber.complete();
|
||||
});
|
||||
getTransaction(id: string): Promise<Transaction> {
|
||||
return Promise.resolve(this.findById(this.transactions, id));
|
||||
}
|
||||
|
||||
createTransaction(
|
||||
budgetId: number,
|
||||
id: string,
|
||||
budgetId: string,
|
||||
name: string,
|
||||
description: string,
|
||||
amount: number,
|
||||
date: Date,
|
||||
isExpense: boolean,
|
||||
category: number
|
||||
): Observable<Transaction> {
|
||||
return Observable.create(subscriber => {
|
||||
category: string
|
||||
): Promise<Transaction> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = new Transaction();
|
||||
transaction.title = name;
|
||||
transaction.description = description;
|
||||
|
@ -252,15 +239,14 @@ export class TwigsLocalService implements TwigsService {
|
|||
transaction.expense = isExpense;
|
||||
transaction.categoryId = category;
|
||||
transaction.budgetId = budgetId;
|
||||
transaction.id = this.transactions.length + 1;
|
||||
transaction.id = randomId();
|
||||
this.transactions.push(transaction);
|
||||
subscriber.next(transaction);
|
||||
subscriber.complete();
|
||||
resolve(transaction);
|
||||
});
|
||||
}
|
||||
|
||||
updateTransaction(budgetId: number, id: number, changes: object): Observable<Transaction> {
|
||||
return Observable.create(subscriber => {
|
||||
updateTransaction(id: string, changes: object): Promise<Transaction> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.findById(this.transactions, id);
|
||||
if (transaction) {
|
||||
const index = this.transactions.indexOf(transaction);
|
||||
|
@ -279,38 +265,71 @@ export class TwigsLocalService implements TwigsService {
|
|||
]
|
||||
);
|
||||
this.transactions[index] = transaction;
|
||||
subscriber.next(transaction);
|
||||
resolve(transaction);
|
||||
} 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> {
|
||||
return Observable.create(subscriber => {
|
||||
deleteTransaction(id: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.findById(this.transactions, id);
|
||||
if (transaction) {
|
||||
const index = this.transactions.indexOf(transaction);
|
||||
delete this.transactions[index];
|
||||
subscriber.complete();
|
||||
resolve();
|
||||
} else {
|
||||
subscriber.error('No transaction found for given id');
|
||||
reject('No transaction found for given id');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Recurring Transactions
|
||||
getRecurringTransactions(
|
||||
budgetId?: string,
|
||||
categoryId?: string,
|
||||
count?: number,
|
||||
from?: Date,
|
||||
to?: Date
|
||||
): Promise<RecurringTransaction[]> {
|
||||
return Promise.reject("Not yet implemented")
|
||||
}
|
||||
|
||||
getRecurringTransaction(id: string): Promise<RecurringTransaction> {
|
||||
return Promise.reject("Not yet implemented")
|
||||
}
|
||||
|
||||
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(): Observable<User> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.error("Not yet implemented")
|
||||
});
|
||||
getProfile(id: string): Promise<User> {
|
||||
return Promise.reject("Not yet implemented");
|
||||
}
|
||||
|
||||
getUsersByUsername(username: string): Observable<User[]> {
|
||||
return Observable.create(subscriber => {
|
||||
subscriber.next(this.users.filter(user => user.username.indexOf(username) > -1 ));
|
||||
});
|
||||
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[]) {
|
||||
|
@ -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 item['id'] === id;
|
||||
})[0];
|
||||
|
|
|
@ -1,56 +1,85 @@
|
|||
import { InjectionToken } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { User, UserPermission } from '../users/user';
|
||||
import { Budget } from '../budgets/budget';
|
||||
import { Category } from '../categories/category';
|
||||
import { RecurringTransaction, Frequency } from '../recurringtransactions/recurringtransaction';
|
||||
import { Transaction } from '../transactions/transaction';
|
||||
|
||||
export interface TwigsService {
|
||||
// Auth
|
||||
login(email: string, password: string): Observable<User>;
|
||||
register(username: string, email: string, password: string): Observable<User>;
|
||||
logout(): Observable<void>;
|
||||
login(email: string, password: string): Promise<User>;
|
||||
register(username: string, email: string, password: string): Promise<User>;
|
||||
logout(): Promise<void>;
|
||||
|
||||
// Budgets
|
||||
getBudgets(): Observable<Budget[]>;
|
||||
getBudget(id: number): Observable<Budget>;
|
||||
getBudgets(): Promise<Budget[]>;
|
||||
getBudget(id: string): Promise<Budget>;
|
||||
getBudgetBalance(id: string, from?: Date, to?: Date): Promise<number>;
|
||||
createBudget(
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
users: UserPermission[],
|
||||
): Observable<Budget>;
|
||||
updateBudget(id: number, changes: object): Observable<Budget>;
|
||||
deleteBudget(id: number): Observable<void>;
|
||||
): Promise<Budget>;
|
||||
updateBudget(id: string, budget: Budget): Promise<Budget>;
|
||||
deleteBudget(id: string): Promise<void>;
|
||||
|
||||
// Categories
|
||||
getCategories(budgetId?: number, count?: number): Observable<Category[]>;
|
||||
getCategory(id: number): Observable<Category>;
|
||||
createCategory(budgetId: number, name: string, amount: number, isExpense: boolean): Observable<Category>;
|
||||
updateCategory(budgetId: number, id: number, changes: object): Observable<Category>;
|
||||
deleteCategory(budgetId: number, id: number): Observable<void>;
|
||||
getCategories(budgetId?: string, count?: number): Promise<Category[]>;
|
||||
getCategory(id: string): Promise<Category>;
|
||||
getCategoryBalance(id: string, from?: Date, to?: Date): Promise<number>;
|
||||
createCategory(id: string, budgetId: string, name: string, description: string, amount: number, isExpense: boolean): Promise<Category>;
|
||||
updateCategory(id: string, category: Category): Promise<Category>;
|
||||
deleteCategory(id: string): Promise<void>;
|
||||
|
||||
// Transactions
|
||||
getTransactions(
|
||||
budgetId?: number,
|
||||
categoryId?: number,
|
||||
budgetId?: string,
|
||||
categoryId?: string,
|
||||
count?: number,
|
||||
from?: Date
|
||||
): Observable<Transaction[]>;
|
||||
getTransaction(id: number): Observable<Transaction>;
|
||||
from?: Date,
|
||||
to?: Date
|
||||
): Promise<Transaction[]>;
|
||||
getTransaction(id: string): Promise<Transaction>;
|
||||
createTransaction(
|
||||
budgetId: number,
|
||||
id: string,
|
||||
budgetId: string,
|
||||
name: string,
|
||||
description: string,
|
||||
amount: number,
|
||||
date: Date,
|
||||
isExpense: boolean,
|
||||
category: number
|
||||
): Observable<Transaction>;
|
||||
updateTransaction(budgetId: number, id: number, changes: object): Observable<Transaction>;
|
||||
deleteTransaction(budgetId: number, id: number): Observable<void>;
|
||||
category: string
|
||||
): Promise<Transaction>;
|
||||
updateTransaction(id: string, transaction: Transaction): Promise<Transaction>;
|
||||
deleteTransaction(id: string): Promise<void>;
|
||||
|
||||
getProfile(): Observable<User>;
|
||||
getUsersByUsername(username: string): Observable<User[]>;
|
||||
// Recurring Transactions
|
||||
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');
|
||||
|
|
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 {
|
||||
padding: 1em;
|
||||
color: #F1F1F1;
|
||||
}
|
||||
|
||||
.transaction-form * {
|
||||
|
@ -10,8 +9,3 @@
|
|||
mat-radio-button {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
|
|
@ -3,16 +3,17 @@
|
|||
</div>
|
||||
<div [hidden]="!currentTransaction" *ngIf="currentTransaction" class="form transaction-form">
|
||||
<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>
|
||||
<textarea matInput [(ngModel)]="currentTransaction.description" placeholder="Description"></textarea>
|
||||
<textarea matInput [(ngModel)]="currentTransaction.description" placeholder="Description" autocapitalize="sentences"></textarea>
|
||||
</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>
|
||||
<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>
|
||||
<input matInput type="time" [(ngModel)]="currentTime" placeholder="Time" required>
|
||||
|
@ -29,5 +30,5 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<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>
|
|
@ -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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('AddEditTransactionComponent', () => {
|
|||
let component: AddEditTransactionComponent;
|
||||
let fixture: ComponentFixture<AddEditTransactionComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddEditTransactionComponent ]
|
||||
})
|
||||
|
|
|
@ -4,7 +4,8 @@ import { TransactionType } from '../transaction.type';
|
|||
import { Category } from 'src/app/categories/category';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
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({
|
||||
selector: 'app-add-edit-transaction',
|
||||
|
@ -14,10 +15,10 @@ import { MatRadioChange } from '@angular/material/radio';
|
|||
export class AddEditTransactionComponent implements OnInit, OnChanges {
|
||||
@Input() title: string;
|
||||
@Input() currentTransaction: Transaction;
|
||||
@Input() budgetId: number;
|
||||
@Input() budgetId: string;
|
||||
@Input() create: boolean
|
||||
public transactionType = TransactionType;
|
||||
public categories: Category[];
|
||||
public rawAmount: string;
|
||||
public currentTime: string;
|
||||
public transactionDate: string;
|
||||
|
||||
|
@ -27,8 +28,8 @@ export class AddEditTransactionComponent implements OnInit, OnChanges {
|
|||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.title = this.title;
|
||||
this.app.backEnabled = true;
|
||||
this.app.setTitle(this.title)
|
||||
this.app.setBackEnabled(true);
|
||||
let d: Date, expense: boolean;
|
||||
if (this.currentTransaction) {
|
||||
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);
|
||||
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.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' });
|
||||
}
|
||||
|
||||
updateCategories(change: MatRadioChange) {
|
||||
this.twigsService.getCategories(this.budgetId)
|
||||
.subscribe(newCategories => {
|
||||
.then(newCategories => {
|
||||
this.categories = newCategories.filter(category => category.expense === change.value)
|
||||
})
|
||||
}
|
||||
|
||||
save(): void {
|
||||
// The amount will be input as a decimal value so we need to convert it
|
||||
// to an integer
|
||||
let observable;
|
||||
let promise;
|
||||
this.currentTransaction.amount = decimalToInteger(String(this.currentTransaction.amount))
|
||||
this.currentTransaction.date = new Date();
|
||||
const dateParts = this.transactionDate.split('-');
|
||||
this.currentTransaction.date.setFullYear(parseInt(dateParts[0], 10));
|
||||
|
@ -71,40 +71,36 @@ export class AddEditTransactionComponent implements OnInit, OnChanges {
|
|||
const timeParts = this.currentTime.split(':');
|
||||
this.currentTransaction.date.setHours(parseInt(timeParts[0], 10));
|
||||
this.currentTransaction.date.setMinutes(parseInt(timeParts[1], 10));
|
||||
if (this.currentTransaction.id) {
|
||||
// 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 {
|
||||
if (this.create) {
|
||||
// This is a new transaction, save it
|
||||
observable = this.twigsService.createTransaction(
|
||||
promise = this.twigsService.createTransaction(
|
||||
this.currentTransaction.id,
|
||||
this.budgetId,
|
||||
this.currentTransaction.title,
|
||||
this.currentTransaction.description,
|
||||
this.currentTransaction.amount * 100,
|
||||
this.currentTransaction.amount,
|
||||
this.currentTransaction.date,
|
||||
this.currentTransaction.expense,
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.twigsService.deleteTransaction(this.budgetId, this.currentTransaction.id).subscribe(() => {
|
||||
this.twigsService.deleteTransaction(this.currentTransaction.id).then(() => {
|
||||
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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('NewTransactionComponent', () => {
|
|||
let component: NewTransactionComponent;
|
||||
let fixture: ComponentFixture<NewTransactionComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ NewTransactionComponent ]
|
||||
})
|
||||
|
|
|
@ -17,7 +17,7 @@ export class NewTransactionComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.budgetId = this.route.snapshot.paramMap.get('budgetId');
|
||||
this.budgetId = this.route.snapshot.queryParamMap.get('budgetId');
|
||||
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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('TransactionDetailsComponent', () => {
|
|||
let component: TransactionDetailsComponent;
|
||||
let fixture: ComponentFixture<TransactionDetailsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ TransactionDetailsComponent ]
|
||||
})
|
||||
|
|
|
@ -10,7 +10,7 @@ import { TWIGS_SERVICE, TwigsService } from 'src/app/shared/twigs.service';
|
|||
})
|
||||
export class TransactionDetailsComponent implements OnInit {
|
||||
|
||||
budgetId: number;
|
||||
budgetId: string;
|
||||
transaction: Transaction;
|
||||
|
||||
constructor(
|
||||
|
@ -23,9 +23,9 @@ export class TransactionDetailsComponent implements OnInit {
|
|||
}
|
||||
|
||||
getTransaction(): void {
|
||||
const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
|
||||
const id = this.route.snapshot.paramMap.get('id');
|
||||
this.twigsService.getTransaction(id)
|
||||
.subscribe(transaction => {
|
||||
.then(transaction => {
|
||||
transaction.amount /= 100;
|
||||
this.transaction = transaction;
|
||||
this.budgetId = transaction.budgetId;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
.transactions .list-row-one {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 0.2em;
|
||||
.transaction-details p {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.transaction-description {
|
||||
font-style: italic;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
<mat-nav-list *ngIf="transactions" class="transactions">
|
||||
<a mat-list-item *ngFor="let transaction of transactions"
|
||||
routerLink="/budgets/{{ budgetId }}/transactions/{{ transaction.id }}">
|
||||
<div matLine class="list-row-one">
|
||||
<p>{{transaction.title}}</p>
|
||||
<a mat-list-item class="transaction-list-item" *ngFor="let transaction of transactions"
|
||||
routerLink="/transactions/{{ transaction.id }}">
|
||||
<div matLine class="transaction-list-details">
|
||||
<p class="transaction-title">{{transaction.title}}</p>
|
||||
<p class="transaction-description text-small" *ngIf="transaction.description">{{transaction.description }}</p>
|
||||
<p matLine class="transaction-date text-small">{{ transaction.date | date }}</p>
|
||||
</div>
|
||||
<p class="amount" [class.expense]="transaction.expense" [class.income]="!transaction.expense">
|
||||
{{ transaction.amount / 100 | currency }}
|
||||
</p>
|
||||
</div>
|
||||
<p matLine class="text-small">{{ transaction.date | date }}</p>
|
||||
</a>
|
||||
</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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('TransactionListComponent', () => {
|
|||
let component: TransactionListComponent;
|
||||
let fixture: ComponentFixture<TransactionListComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ TransactionListComponent ]
|
||||
})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Component, OnInit, Input, Inject } from '@angular/core';
|
||||
import { Transaction } from '../transaction';
|
||||
import { TWIGS_SERVICE, TwigsService } from '../../shared/twigs.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction-list',
|
||||
|
@ -9,12 +10,13 @@ import { TWIGS_SERVICE, TwigsService } from '../../shared/twigs.service';
|
|||
})
|
||||
export class TransactionListComponent implements OnInit {
|
||||
|
||||
@Input() budgetId: number;
|
||||
@Input() categoryId?: number;
|
||||
@Input() budgetIds: string[];
|
||||
@Input() categoryIds?: string[];
|
||||
public transactions: Transaction[];
|
||||
|
||||
constructor(
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
private route: ActivatedRoute
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -22,13 +24,36 @@ export class TransactionListComponent implements OnInit {
|
|||
}
|
||||
|
||||
getTransactions(): void {
|
||||
let fromStr = this.route.snapshot.queryParamMap.get('from');
|
||||
var from;
|
||||
if (fromStr) {
|
||||
let fromDate = new Date(fromStr);
|
||||
if (!isNaN(fromDate.getTime())) {
|
||||
from = fromDate;
|
||||
}
|
||||
}
|
||||
|
||||
if (!from) {
|
||||
let date = new Date();
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
date.setDate(1);
|
||||
this.twigsService.getTransactions(this.budgetId, this.categoryId, null, date).subscribe(transactions => {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { randomId } from '../shared/utils';
|
||||
|
||||
export class Transaction {
|
||||
id: number;
|
||||
id: string = randomId();
|
||||
title: string;
|
||||
description: string = null;
|
||||
date: Date = new Date();
|
||||
amount: number;
|
||||
expense = true;
|
||||
categoryId: number;
|
||||
budgetId: number;
|
||||
createdBy: number;
|
||||
categoryId: string;
|
||||
budgetId: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<app-transaction-list [budgetId]="budgetId" [categoryId]="categoryId"></app-transaction-list>
|
||||
<a mat-fab routerLink="/budgets/{{ budgetId }}/transactions/new">
|
||||
<app-transaction-list [budgetIds]="[budgetId]" [categoryIds]="categoryId"></app-transaction-list>
|
||||
<a mat-fab routerLink="/transactions/new" [queryParams]="{budgetId: budgetId}">
|
||||
<mat-icon aria-label="Add">add</mat-icon>
|
||||
</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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('ExpensesComponent', () => {
|
|||
let component: TransactionsComponent;
|
||||
let fixture: ComponentFixture<TransactionsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ TransactionsComponent ]
|
||||
})
|
||||
|
|
|
@ -9,8 +9,8 @@ import { ActivatedRoute } from '@angular/router';
|
|||
})
|
||||
export class TransactionsComponent implements OnInit {
|
||||
|
||||
budgetId: number;
|
||||
categoryId?: number;
|
||||
budgetId?: string;
|
||||
categoryId?: string;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
|
@ -18,9 +18,9 @@ export class TransactionsComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.budgetId = Number.parseInt(this.route.snapshot.paramMap.get('budgetId'));
|
||||
this.categoryId = Number.parseInt(this.route.snapshot.queryParamMap.get('categoryId'));
|
||||
this.app.backEnabled = true;
|
||||
this.app.title = 'Transactions';
|
||||
this.budgetId = this.route.snapshot.queryParamMap.get('budgetIds');
|
||||
this.categoryId = this.route.snapshot.queryParamMap.get('categoryIds');
|
||||
this.app.setBackEnabled(true);
|
||||
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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('EditProfileComponent', () => {
|
|||
let component: EditProfileComponent;
|
||||
let fixture: ComponentFixture<EditProfileComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('LoginComponent', () => {
|
|||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent ]
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, Inject, ChangeDetectorRef } from '@angula
|
|||
import { TwigsService, TWIGS_SERVICE } from '../../shared/twigs.service';
|
||||
import { User } from '../user';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
|
@ -14,27 +14,31 @@ export class LoginComponent implements OnInit {
|
|||
public isLoading = false;
|
||||
public email: string;
|
||||
public password: string;
|
||||
private redirect: string;
|
||||
|
||||
constructor(
|
||||
private app: AppComponent,
|
||||
@Inject(TWIGS_SERVICE) private twigsService: TwigsService,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.title = 'Login';
|
||||
this.app.backEnabled = true;
|
||||
this.app.setTitle('Login')
|
||||
this.app.setBackEnabled(true);
|
||||
this.redirect = this.activatedRoute.snapshot.queryParamMap.get('redirect');
|
||||
}
|
||||
|
||||
login(): void {
|
||||
this.isLoading = true;
|
||||
this.twigsService.login(this.email, this.password)
|
||||
.subscribe(user => {
|
||||
.then(user => {
|
||||
this.app.user.next(user);
|
||||
this.router.navigate(['/'])
|
||||
},
|
||||
error => {
|
||||
this.router.navigate([this.redirect || '/'])
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
// TODO: Replace this with an in-app dialog
|
||||
alert("Login failed. Please verify you have the correct credentials");
|
||||
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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('RegisterComponent', () => {
|
|||
let component: RegisterComponent;
|
||||
let fixture: ComponentFixture<RegisterComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ RegisterComponent ]
|
||||
})
|
||||
|
|
|
@ -23,8 +23,8 @@ export class RegisterComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.app.title = 'Register';
|
||||
this.app.backEnabled = true;
|
||||
this.app.setTitle('Register')
|
||||
this.app.setBackEnabled(true);
|
||||
}
|
||||
|
||||
register(): void {
|
||||
|
@ -33,10 +33,10 @@ export class RegisterComponent implements OnInit {
|
|||
return;
|
||||
}
|
||||
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);
|
||||
this.router.navigate(['/'])
|
||||
}, error => {
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
alert("Registration failed!")
|
||||
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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('UserComponent', () => {
|
|||
let component: UserComponent;
|
||||
let fixture: ComponentFixture<UserComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UserComponent ]
|
||||
})
|
||||
|
|
|
@ -1,27 +1,36 @@
|
|||
import { randomId } from "../shared/utils";
|
||||
|
||||
export class User {
|
||||
id: number;
|
||||
id: string = randomId();
|
||||
username: string;
|
||||
email: string;
|
||||
|
||||
constructor(id?: number, username?: string, email?: string) {
|
||||
constructor(id?: string, username?: string, email?: string) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthToken {
|
||||
userId: string;
|
||||
token: string;
|
||||
expiration: Date;
|
||||
}
|
||||
|
||||
export class UserPermission {
|
||||
user: number;
|
||||
user: string;
|
||||
permission: Permission;
|
||||
|
||||
constructor(user: number, permission: Permission) {
|
||||
constructor(user: string, permission: Permission) {
|
||||
this.user = user;
|
||||
this.permission = permission;
|
||||
}
|
||||
}
|
||||
|
||||
export enum Permission {
|
||||
READ,
|
||||
WRITE,
|
||||
OWNER
|
||||
READ = "READ",
|
||||
WRITE = "WRITE",
|
||||
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 = {
|
||||
production: true,
|
||||
apiUrl: 'https://api.twigs.brawner.dev'
|
||||
apiUrl: 'https://twigs.api.wbrawner.com/api'
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
export const environment = {
|
||||
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
|
||||
* 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="application-name" content="Twigs">
|
||||
<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 rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
|
|
|
@ -18,41 +18,6 @@
|
|||
* 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
|
||||
* 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.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
html,
|
||||
body {
|
||||
background: #333333;
|
||||
background: #F1F1F1;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #F1F1F1;
|
||||
color: #333333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
@ -20,8 +20,8 @@ a.mat-fab {
|
|||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 1em;
|
||||
color: #BDBDBD
|
||||
font-size: 90%;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
mat-toolbar.mat-toolbar-row,
|
||||
|
@ -29,21 +29,21 @@ mat-toolbar.mat-toolbar-single-row {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
mat-toolbar > span {
|
||||
mat-toolbar>span {
|
||||
display: flex;
|
||||
width: 33%;
|
||||
;
|
||||
}
|
||||
|
||||
mat-toolbar > span:nth-child(1) {
|
||||
mat-toolbar>span:nth-child(1) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
mat-toolbar > span:nth-child(2) {
|
||||
mat-toolbar>span:nth-child(2) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-toolbar > span:nth-child(3) {
|
||||
mat-toolbar>span:nth-child(3) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
@ -105,7 +105,7 @@ mat-sidenav {
|
|||
|
||||
.form {
|
||||
padding: 1em;
|
||||
color: #F1F1F1;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.form .mat-form-field,
|
||||
|
@ -116,3 +116,28 @@ mat-sidenav {
|
|||
.form mat-radio-button {
|
||||
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