├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── pipeline.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── docker-compose.yml └── src ├── .browserslistrc ├── .dockerignore ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── README.md ├── angular.json ├── docker.entrypoint.sh ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── main.electron.js ├── nginx.conf ├── ngsw-config.json ├── package-lock.json ├── package.json ├── setup-electron.js ├── src ├── _redirects ├── app │ ├── Components │ │ ├── component.module.ts │ │ ├── error-page │ │ │ ├── error-page.component.css │ │ │ ├── error-page.component.html │ │ │ ├── error-page.component.spec.ts │ │ │ └── error-page.component.ts │ │ ├── goal-form │ │ │ ├── goal-form.component.css │ │ │ ├── goal-form.component.html │ │ │ ├── goal-form.component.spec.ts │ │ │ └── goal-form.component.ts │ │ ├── loading-spinner │ │ │ └── loading-spinner.component.ts │ │ ├── material.module.ts │ │ ├── notifications │ │ │ ├── notifications.component.css │ │ │ ├── notifications.component.html │ │ │ ├── notifications.component.spec.ts │ │ │ └── notifications.component.ts │ │ ├── table-formatters │ │ │ └── IconFormatterComponent.ts │ │ ├── transaction-form │ │ │ ├── transaction-form.component.css │ │ │ ├── transaction-form.component.html │ │ │ ├── transaction-form.component.spec.ts │ │ │ └── transaction-form.component.ts │ │ └── widgets │ │ │ ├── balance-history-chart │ │ │ ├── balance-history-chart.component.css │ │ │ ├── balance-history-chart.component.html │ │ │ ├── balance-history-chart.component.spec.ts │ │ │ └── balance-history-chart.component.ts │ │ │ └── spent-per-category-chart │ │ │ ├── spent-per-category-chart.component.css │ │ │ ├── spent-per-category-chart.component.html │ │ │ ├── spent-per-category-chart.component.spec.ts │ │ │ └── spent-per-category-chart.component.ts │ ├── Guards │ │ ├── auth.guard.ts │ │ └── demo.guard.ts │ ├── Models │ │ ├── account-settings.ts │ │ ├── account.model.ts │ │ ├── asset.model.ts │ │ ├── line.chart.ts │ │ ├── pie.chart.ts │ │ └── trade.model.ts │ ├── Modules │ │ └── wealth │ │ │ ├── Pages │ │ │ ├── wealth-assets │ │ │ │ ├── wealth-assets.component.css │ │ │ │ ├── wealth-assets.component.html │ │ │ │ ├── wealth-assets.component.spec.ts │ │ │ │ └── wealth-assets.component.ts │ │ │ ├── wealth-portfolio │ │ │ │ ├── wealth-portfolio.component.css │ │ │ │ ├── wealth-portfolio.component.html │ │ │ │ ├── wealth-portfolio.component.spec.ts │ │ │ │ └── wealth-portfolio.component.ts │ │ │ └── wealth-trades │ │ │ │ ├── wealth-trades.component.css │ │ │ │ ├── wealth-trades.component.html │ │ │ │ ├── wealth-trades.component.spec.ts │ │ │ │ └── wealth-trades.component.ts │ │ │ ├── wealth-routing.module.ts │ │ │ └── wealth.module.ts │ ├── Navigation │ │ ├── MenuItems.ts │ │ ├── navigation.component.css │ │ ├── navigation.component.html │ │ └── navigation.component.ts │ ├── Pages │ │ ├── account-details │ │ │ ├── account-details-external-accounts │ │ │ │ ├── account-details-external-accounts.component.css │ │ │ │ ├── account-details-external-accounts.component.html │ │ │ │ ├── account-details-external-accounts.component.spec.ts │ │ │ │ └── account-details-external-accounts.component.ts │ │ │ ├── account-details.component.css │ │ │ ├── account-details.component.html │ │ │ ├── account-details.component.spec.ts │ │ │ └── account-details.component.ts │ │ ├── accounts │ │ │ ├── accounts.component.css │ │ │ ├── accounts.component.html │ │ │ ├── accounts.component.spec.ts │ │ │ └── accounts.component.ts │ │ ├── add │ │ │ ├── add-account │ │ │ │ ├── add-account.component.css │ │ │ │ ├── add-account.component.html │ │ │ │ ├── add-account.component.spec.ts │ │ │ │ └── add-account.component.ts │ │ │ ├── add-split-transaction │ │ │ │ ├── add-split-transaction.component.css │ │ │ │ ├── add-split-transaction.component.html │ │ │ │ ├── add-split-transaction.component.spec.ts │ │ │ │ └── add-split-transaction.component.ts │ │ │ ├── add-transaction │ │ │ │ ├── add-transaction.component.css │ │ │ │ ├── add-transaction.component.html │ │ │ │ ├── add-transaction.component.spec.ts │ │ │ │ └── add-transaction.component.ts │ │ │ └── add-transfer-transaction │ │ │ │ ├── add-transfer-transaction.component.css │ │ │ │ ├── add-transfer-transaction.component.html │ │ │ │ ├── add-transfer-transaction.component.spec.ts │ │ │ │ └── add-transfer-transaction.component.ts │ │ ├── dashboard │ │ │ ├── dashboard.component.css │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.spec.ts │ │ │ └── dashboard.component.ts │ │ ├── datafeeds │ │ │ ├── datafeed-coinbase │ │ │ │ ├── datafeed-coinbase.component.css │ │ │ │ ├── datafeed-coinbase.component.html │ │ │ │ ├── datafeed-coinbase.component.spec.ts │ │ │ │ └── datafeed-coinbase.component.ts │ │ │ ├── datafeed-truelayer │ │ │ │ ├── datafeed-truelayer.component.css │ │ │ │ ├── datafeed-truelayer.component.html │ │ │ │ ├── datafeed-truelayer.component.spec.ts │ │ │ │ └── datafeed-truelayer.component.ts │ │ │ ├── datafeeds-list │ │ │ │ ├── datafeeds-list.component.css │ │ │ │ ├── datafeeds-list.component.html │ │ │ │ ├── datafeeds-list.component.spec.ts │ │ │ │ └── datafeeds-list.component.ts │ │ │ ├── datafeeds-routing.module.ts │ │ │ ├── datafeeds.component.css │ │ │ ├── datafeeds.component.html │ │ │ ├── datafeeds.component.spec.ts │ │ │ ├── datafeeds.component.ts │ │ │ └── datafeeds.module.ts │ │ ├── goals │ │ │ ├── add-goal │ │ │ │ ├── add-goal.component.css │ │ │ │ ├── add-goal.component.html │ │ │ │ ├── add-goal.component.spec.ts │ │ │ │ └── add-goal.component.ts │ │ │ ├── edit-goal │ │ │ │ ├── edit-goal.component.css │ │ │ │ ├── edit-goal.component.html │ │ │ │ ├── edit-goal.component.spec.ts │ │ │ │ └── edit-goal.component.ts │ │ │ ├── goals.component.css │ │ │ ├── goals.component.html │ │ │ ├── goals.component.spec.ts │ │ │ └── goals.component.ts │ │ ├── login │ │ │ ├── login.component.css │ │ │ ├── login.component.html │ │ │ ├── login.component.spec.ts │ │ │ └── login.component.ts │ │ ├── register │ │ │ ├── register.component.css │ │ │ ├── register.component.html │ │ │ ├── register.component.spec.ts │ │ │ └── register.component.ts │ │ ├── settings │ │ │ ├── settings.component.css │ │ │ ├── settings.component.html │ │ │ ├── settings.component.spec.ts │ │ │ └── settings.component.ts │ │ ├── transaction-details │ │ │ ├── transaction-details.component.css │ │ │ ├── transaction-details.component.html │ │ │ ├── transaction-details.component.spec.ts │ │ │ └── transaction-details.component.ts │ │ └── transactions │ │ │ ├── transactions.component.css │ │ │ ├── transactions.component.html │ │ │ ├── transactions.component.spec.ts │ │ │ └── transactions.component.ts │ ├── Pipes │ │ └── pipes.module.ts │ ├── Services │ │ ├── accounts.service.spec.ts │ │ ├── accounts.service.ts │ │ ├── auth.service.spec.ts │ │ ├── auth.service.ts │ │ ├── config.service.spec.ts │ │ ├── config.service.ts │ │ ├── datafeeds.service.spec.ts │ │ ├── datafeeds.service.ts │ │ ├── finance-api.request.service.spec.ts │ │ ├── finance-api.request.service.ts │ │ ├── goals.service.spec.ts │ │ ├── goals.service.ts │ │ ├── notification.service.spec.ts │ │ ├── notification.service.ts │ │ ├── service.module.ts │ │ ├── statistics.service.spec.ts │ │ ├── statistics.service.ts │ │ ├── theme.service.ts │ │ ├── title.service.spec.ts │ │ ├── title.service.ts │ │ ├── transactions.service.spec.ts │ │ ├── transactions.service.ts │ │ └── wealth │ │ │ ├── assets.service.ts │ │ │ └── trades.service.ts │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ └── app.module.ts ├── assets │ ├── .gitkeep │ ├── city.jpg │ ├── config.schema.json │ ├── defaultTransaction.png │ ├── demo.config.json │ ├── icons │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png │ ├── logo_square.png │ ├── preview.png │ └── template.config.json ├── default.theme.scss ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── manifest.webmanifest ├── polyfills.ts ├── styles.css └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Pipeline 2 | on: [push] 3 | 4 | jobs: 5 | Build_Docker_Image: 6 | name: Build Docker Image 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v1 11 | with: 12 | fetch-depth: 1 13 | 14 | - uses: actions/cache@v2 15 | with: 16 | path: ~/.npm 17 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 18 | restore-keys: | 19 | ${{ runner.os }}-node- 20 | 21 | - name: Extract branch name 22 | shell: bash 23 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 24 | id: extract_branch 25 | 26 | - name: Docker build 27 | run: | 28 | cd src/ 29 | docker login docker.pkg.github.com -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" 30 | docker login -u "$DOCKERHUB_USERNAME" -p "$DOCKERHUB_PASSWORD" 31 | docker build -t docker.pkg.github.com/benfl3713/finance-manager/finance-manager:latest . 32 | env: 33 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 34 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 35 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} 36 | DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} 37 | 38 | - name: Docker Publish 39 | if: github.ref == 'refs/heads/master' 40 | run: | 41 | docker push docker.pkg.github.com/benfl3713/finance-manager/finance-manager:latest 42 | docker tag docker.pkg.github.com/benfl3713/finance-manager/finance-manager:latest $DOCKER_USERNAME/finance-manager 43 | docker push $DOCKER_USERNAME/finance-manager 44 | env: 45 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 46 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Chrome", 9 | "request": "launch", 10 | "type": "pwa-chrome", 11 | "url": "http://localhost:4200", 12 | "webRoot": "${workspaceFolder}/src" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/.vscode/settings.json -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "build", 9 | "path": "src/", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | }, 14 | "problemMatcher": [], 15 | "label": "npm: build", 16 | "detail": "ng build" 17 | }, 18 | { 19 | "type": "npm", 20 | "script": "start", 21 | "path": "src/", 22 | "problemMatcher": [], 23 | "label": "npm: start", 24 | "detail": "ng start" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Finance-Manager 2 | 3 | ![](https://github.com/benfl3713/Finance-Manager/workflows/Pipeline/badge.svg?branch=master) 4 | [![Netlify Status](https://api.netlify.com/api/v1/badges/6851da60-c445-4cc1-beb3-7f0ff4d69943/deploy-status)](https://app.netlify.com/sites/demo-finance-manager-benfl3713/deploys) 5 | 6 | Personal Finance Manager Web App 7 | 8 | ![](https://github.com/benfl3713/Finance-Manager/blob/master/src/src/assets/preview.png?raw=true) 9 | 10 | # Purpose 11 | 12 | This is a webapp that allows multiple users to add accounts and transactions etc and manage your finances. 13 | 14 | # Use 15 | 16 | - This webapp integrates with the https://github.com/benfl3713/finance-api to manage all data. 17 | - It is required that both this and the finance-api are running and connected together in order to load any data. To do so add the api url to the config.json file in the assets folder. (You will need to create it for the first time, there is a demo one in the same folder) 18 | - You can easily run this in docker using the image: benfl3713/finance-manager and also run the api image which is detailed in the api repo 19 | 20 | # Technology 21 | 22 | - angular front end with a angular material theme 23 | 24 | # Development 25 | 26 | 1. Clone repository 27 | 2. Open a terminal in the `src` folder 28 | 3. Run `npm install` to install all dependencies 29 | 4. Run `npm start` to start the site up 30 | 31 | # Need Help? 32 | 33 | - If you have any questions feel free to raise an issue or email me at **benfl3713@gmail.com** 34 | - I can also give you a demo of a demo site I have running if you're intrested 35 | 36 | # Docker 37 | 38 | If you want to run the finance manager and finance api and database all together then you can use the following docker-compose configuration. 39 | 40 | ```yaml 41 | version: "3" 42 | 43 | services: 44 | finance-manager: 45 | image: benfl3713/finance-manager:latest 46 | depends_on: 47 | - finance-api 48 | ports: 49 | - "5005:80" 50 | environment: 51 | FinanceApiUrl: "http://localhost:5001/api" 52 | finance-api: 53 | image: benfl3713/finance-api:latest 54 | depends_on: 55 | - mongo-db 56 | ports: 57 | - "5001:80" 58 | volumes: 59 | - financeApi:/app/config 60 | environment: 61 | MongoDB_ConnectionString: "mongodb://mongo-db" 62 | mongo-db: 63 | image: mongo 64 | volumes: 65 | - financeDatabase:/data/db 66 | 67 | volumes: 68 | financeApi: 69 | financeDatabase: 70 | ``` 71 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | finance-manager: 5 | build: 6 | context: ./src 7 | dockerfile: "Dockerfile" 8 | image: finance-manager:latest 9 | ports: 10 | - "8080:80" 11 | environment: 12 | FinanceApiUrl: "http://localhost:5001" 13 | -------------------------------------------------------------------------------- /src/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /src/.dockerignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/explicit-module-boundary-types": "off", 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /finance-manager-win32-x64 8 | /installers 9 | /windows_installer 10 | # Only exists if Bazel was run 11 | /bazel-out 12 | /electron 13 | 14 | # dependencies 15 | /node_modules 16 | src/assets/config.json 17 | 18 | # profiling files 19 | chrome-profiler-events*.json 20 | speed-measure-plugin*.json 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | .history/* 38 | 39 | # misc 40 | /.sass-cache 41 | /connect.lock 42 | /coverage 43 | /libpeerconnection.log 44 | npm-debug.log 45 | yarn-error.log 46 | testem.log 47 | /typings 48 | 49 | # System Files 50 | .DS_Store 51 | Thumbs.db 52 | -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as build-step 2 | WORKDIR /app 3 | COPY . . 4 | 5 | RUN npm install 6 | RUN npm run build:prod 7 | 8 | # Final Image 9 | FROM nginx:1.19.2-alpine 10 | COPY --from=build-step /app/dist /usr/share/nginx/html 11 | COPY --from=build-step /app/nginx.conf /etc/nginx/conf.d/default.conf 12 | COPY docker.entrypoint.sh /launch/docker.entrypoint.sh 13 | RUN chmod +x /launch/docker.entrypoint.sh 14 | EXPOSE 80 15 | # run nginx 16 | ENTRYPOINT ["/launch/docker.entrypoint.sh"] 17 | CMD ["nginx", "-g", "daemon off;"] 18 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # FinanceManager 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.0.8. 4 | 5 | ## Development server 6 | 7 | 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. 8 | 9 | ## Code scaffolding 10 | 11 | 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`. 12 | 13 | ## Build 14 | 15 | 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. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | 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). 28 | -------------------------------------------------------------------------------- /src/docker.entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -eu 3 | 4 | envsubst < /usr/share/nginx/html/assets/template.config.json > /usr/share/nginx/html/assets/config.json 5 | 6 | exec "$@" 7 | -------------------------------------------------------------------------------- /src/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ 31 | spec: { 32 | displayStacktrace: StacktraceOption.PRETTY 33 | } 34 | })); 35 | } 36 | }; -------------------------------------------------------------------------------- /src/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('finance-manager app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/finance-manager'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name finance-manager; 4 | location / { 5 | root /usr/share/nginx/html; 6 | try_files $uri /index.html; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/setup-electron.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | const configPath = "dist/assets/config.json"; 5 | if (!fs.existsSync(configPath)) { 6 | fs.rmSync(configPath); 7 | } 8 | 9 | const electronDefaultConfig = { 10 | $schema: "./config.schema.json", 11 | FinanceApiUrl: "http://localhost:5001/api", 12 | IsDemo: false, 13 | }; 14 | 15 | fs.writeFileSync(configPath, JSON.stringify(electronDefaultConfig, null, 2)); 16 | -------------------------------------------------------------------------------- /src/src/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /src/src/app/Components/component.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { CommonModule, CurrencyPipe, DatePipe } from '@angular/common'; 4 | import { FlexLayoutModule } from '@angular/flex-layout'; 5 | import { TransactionFormComponent } from './transaction-form/transaction-form.component'; 6 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 7 | import { MaterialModule } from './material.module'; 8 | import { BalanceHistoryChartComponent } from './widgets/balance-history-chart/balance-history-chart.component'; 9 | import { LoadingSpinnerComponent } from './loading-spinner/loading-spinner.component'; 10 | import { SpentPerCategoryChartComponent } from './widgets/spent-per-category-chart/spent-per-category-chart.component'; 11 | import { ErrorPageComponent } from './error-page/error-page.component'; 12 | import { GoalFormComponent } from './goal-form/goal-form.component'; 13 | import { NotificationsComponent } from "./notifications/notifications.component"; 14 | 15 | @NgModule({ 16 | declarations: [ 17 | TransactionFormComponent, 18 | BalanceHistoryChartComponent, 19 | LoadingSpinnerComponent, 20 | SpentPerCategoryChartComponent, 21 | ErrorPageComponent, 22 | GoalFormComponent, 23 | NotificationsComponent 24 | ], 25 | imports: [ 26 | CommonModule, 27 | FormsModule, 28 | ReactiveFormsModule, 29 | MaterialModule, 30 | FlexLayoutModule, 31 | RouterModule, 32 | ], 33 | providers: [DatePipe, CurrencyPipe], 34 | exports: [ 35 | TransactionFormComponent, 36 | BalanceHistoryChartComponent, 37 | LoadingSpinnerComponent, 38 | SpentPerCategoryChartComponent, 39 | ErrorPageComponent, 40 | GoalFormComponent, 41 | NotificationsComponent 42 | ], 43 | }) 44 | export class ComponentModule {} 45 | -------------------------------------------------------------------------------- /src/src/app/Components/error-page/error-page.component.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | box-sizing: border-box; 4 | } 5 | 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | } 10 | 11 | #notfound { 12 | position: relative; 13 | height: 100vh; 14 | } 15 | 16 | #notfound .notfound { 17 | position: absolute; 18 | left: 50%; 19 | top: 50%; 20 | -webkit-transform: translate(-50%, -50%); 21 | -ms-transform: translate(-50%, -50%); 22 | transform: translate(-50%, -50%); 23 | } 24 | 25 | .notfound { 26 | max-width: 767px; 27 | width: 100%; 28 | line-height: 1.4; 29 | padding: 0px 15px; 30 | } 31 | 32 | .notfound .notfound-404 { 33 | position: relative; 34 | height: 150px; 35 | line-height: 150px; 36 | margin-bottom: 25px; 37 | } 38 | 39 | .notfound .notfound-404 h1 { 40 | font-family: "Titillium Web", sans-serif; 41 | font-size: 186px; 42 | font-weight: 900; 43 | margin: 0px; 44 | } 45 | 46 | .notfound h2 { 47 | font-family: "Titillium Web", sans-serif; 48 | font-size: 26px; 49 | font-weight: 700; 50 | margin: 0; 51 | } 52 | 53 | .notfound p { 54 | font-family: "Montserrat", sans-serif; 55 | font-size: 14px; 56 | font-weight: 500; 57 | margin-bottom: 0px; 58 | text-transform: uppercase; 59 | } 60 | 61 | .notfound a { 62 | font-family: "Titillium Web", sans-serif; 63 | display: inline-block; 64 | text-transform: uppercase; 65 | color: #fff; 66 | text-decoration: none; 67 | border: none; 68 | background: #5c91fe; 69 | padding: 10px 40px; 70 | font-size: 14px; 71 | font-weight: 700; 72 | border-radius: 1px; 73 | margin-top: 15px; 74 | -webkit-transition: 0.2s all; 75 | transition: 0.2s all; 76 | } 77 | 78 | .notfound a:hover { 79 | opacity: 0.8; 80 | } 81 | 82 | @media only screen and (max-width: 767px) { 83 | .notfound .notfound-404 { 84 | height: 110px; 85 | line-height: 110px; 86 | } 87 | .notfound .notfound-404 h1 { 88 | font-size: 120px; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/src/app/Components/error-page/error-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

404

5 |
6 |

Oops! This Page Could Not Be Found

7 |

8 | Sorry but the page you are looking for does not exist, have been removed. 9 | name changed or is temporarily unavailable 10 |

11 | Go To Homepage 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/src/app/Components/error-page/error-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ErrorPageComponent } from './error-page.component'; 4 | 5 | describe('ErrorPageComponent', () => { 6 | let component: ErrorPageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ErrorPageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ErrorPageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Components/error-page/error-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './error-page.component.html', 5 | styleUrls: ['./error-page.component.css'] 6 | }) 7 | export class ErrorPageComponent implements OnInit { 8 | 9 | constructor() { } 10 | 11 | ngOnInit(): void { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/src/app/Components/goal-form/goal-form.component.css: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | button { 5 | margin-right: 10px; 6 | } 7 | 8 | @media (min-width: 601px) { 9 | .row { 10 | margin-left: 2px; 11 | } 12 | } 13 | 14 | form { 15 | max-width: 1000px; 16 | } 17 | -------------------------------------------------------------------------------- /src/src/app/Components/goal-form/goal-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Name 4 | 5 | 6 |
7 | 8 | 9 | Account 10 | 15 | 16 | {{ account.AccountName }} 17 | 18 | 19 | 20 |
21 | 22 | 23 | Amount 24 | 25 | 26 |
27 | 28 | 29 | Date 30 | 31 | Date Required 32 | 33 |
34 | 35 |
36 | 45 | 48 |
49 |
50 | -------------------------------------------------------------------------------- /src/src/app/Components/goal-form/goal-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { GoalFormComponent } from './goal-form.component'; 4 | 5 | describe('GoalFormComponent', () => { 6 | let component: GoalFormComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ GoalFormComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(GoalFormComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Components/goal-form/goal-form.component.ts: -------------------------------------------------------------------------------- 1 | import { DatePipe } from '@angular/common'; 2 | import { Component, EventEmitter, OnInit, Output } from '@angular/core'; 3 | import { FormControl, FormGroup, Validators } from '@angular/forms'; 4 | import { Router } from '@angular/router'; 5 | import { map } from 'rxjs/operators'; 6 | import { AccountsService } from 'src/app/Services/accounts.service'; 7 | 8 | @Component({ 9 | selector: 'app-goal-form', 10 | templateUrl: './goal-form.component.html', 11 | styleUrls: ['./goal-form.component.css'], 12 | }) 13 | export class GoalFormComponent implements OnInit { 14 | constructor( 15 | private accountsService: AccountsService, 16 | private router: Router, 17 | private datePipe: DatePipe 18 | ) {} 19 | 20 | @Output() save: EventEmitter = new EventEmitter(); 21 | 22 | accounts$ = this.accountsService.getAccounts().pipe( 23 | map((accounts) => 24 | accounts.map((a) => { 25 | return { AccountId: a.ID, AccountName: a.AccountName }; 26 | }) 27 | ) 28 | ); 29 | 30 | goalForm = new FormGroup({ 31 | name: new FormControl(null, [Validators.required]), 32 | date: new FormControl(null, [Validators.required]), 33 | account: new FormControl({ AccountID: null, AccountName: null }, [ 34 | Validators.required, 35 | ]), 36 | amount: new FormControl(null, [Validators.required]), 37 | }); 38 | 39 | ngOnInit(): void {} 40 | 41 | submit() { 42 | this.disable(); 43 | this.save.emit(this.goalForm); 44 | } 45 | 46 | cancel() { 47 | if (window.history.length > 0) { 48 | window.history.back(); 49 | } else { 50 | this.router.navigate(['/transactions']); 51 | } 52 | } 53 | 54 | enable() { 55 | this.goalForm.enable(); 56 | } 57 | 58 | disable() { 59 | this.goalForm.disable(); 60 | } 61 | 62 | accountComparer(a1, a2) { 63 | return a1.AccountName == a2.AccountName && a1.AccountId == a2.AccountId; 64 | } 65 | 66 | setFormValues(goal: any) { 67 | try { 68 | this.goalForm.controls.date.setValue( 69 | this.datePipe.transform(goal.Date, 'yyyy-MM-dd') 70 | ); 71 | this.goalForm.controls.account.setValue({ 72 | AccountId: goal.AccountId, 73 | AccountName: goal.AccountName, 74 | }); 75 | this.goalForm.controls.amount.setValue(goal.GoalAmount); 76 | this.goalForm.controls.name.setValue(goal.Name); 77 | } catch (ex) { 78 | console.log(ex); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/src/app/Components/loading-spinner/loading-spinner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'loading-spinner', 5 | template: ` 6 |
12 | 13 |
14 | `, 15 | }) 16 | export class LoadingSpinnerComponent { 17 | @Input() isLoading: boolean = true; 18 | @Input() diameter: number = 100; 19 | } 20 | -------------------------------------------------------------------------------- /src/src/app/Components/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { A11yModule } from '@angular/cdk/a11y'; 3 | import { PortalModule } from '@angular/cdk/portal'; 4 | import { ScrollingModule } from '@angular/cdk/scrolling'; 5 | import { CdkStepperModule } from '@angular/cdk/stepper'; 6 | import { CdkTableModule } from '@angular/cdk/table'; 7 | import { CdkTreeModule } from '@angular/cdk/tree'; 8 | import { MatAutocompleteModule } from '@angular/material/autocomplete'; 9 | import { MatBadgeModule } from '@angular/material/badge'; 10 | import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; 11 | import { MatButtonModule } from '@angular/material/button'; 12 | import { MatButtonToggleModule } from '@angular/material/button-toggle'; 13 | import { MatCardModule } from '@angular/material/card'; 14 | import { MatCheckboxModule } from '@angular/material/checkbox'; 15 | import { MatChipsModule } from '@angular/material/chips'; 16 | import { MatStepperModule } from '@angular/material/stepper'; 17 | import { MatDatepickerModule } from '@angular/material/datepicker'; 18 | import { MatDialogModule } from '@angular/material/dialog'; 19 | import { MatDividerModule } from '@angular/material/divider'; 20 | import { MatExpansionModule } from '@angular/material/expansion'; 21 | import { MatGridListModule } from '@angular/material/grid-list'; 22 | import { MatIconModule } from '@angular/material/icon'; 23 | import { MatInputModule } from '@angular/material/input'; 24 | import { MatListModule } from '@angular/material/list'; 25 | import { MatMenuModule } from '@angular/material/menu'; 26 | import { MatNativeDateModule, MatRippleModule } from '@angular/material/core'; 27 | import { MatPaginatorModule } from '@angular/material/paginator'; 28 | import { MatProgressBarModule } from '@angular/material/progress-bar'; 29 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 30 | import { MatRadioModule } from '@angular/material/radio'; 31 | import { MatSelectModule } from '@angular/material/select'; 32 | import { MatSidenavModule } from '@angular/material/sidenav'; 33 | import { MatSliderModule } from '@angular/material/slider'; 34 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 35 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 36 | import { MatSortModule } from '@angular/material/sort'; 37 | import { MatTableModule } from '@angular/material/table'; 38 | import { MatTabsModule } from '@angular/material/tabs'; 39 | import { MatToolbarModule } from '@angular/material/toolbar'; 40 | import { MatTooltipModule } from '@angular/material/tooltip'; 41 | 42 | @NgModule({ 43 | exports: [ 44 | A11yModule, 45 | CdkStepperModule, 46 | CdkTableModule, 47 | CdkTreeModule, 48 | MatAutocompleteModule, 49 | MatBadgeModule, 50 | MatBottomSheetModule, 51 | MatButtonModule, 52 | MatButtonToggleModule, 53 | MatCardModule, 54 | MatCheckboxModule, 55 | MatChipsModule, 56 | MatStepperModule, 57 | MatDatepickerModule, 58 | MatDialogModule, 59 | MatDividerModule, 60 | MatExpansionModule, 61 | MatGridListModule, 62 | MatIconModule, 63 | MatInputModule, 64 | MatListModule, 65 | MatMenuModule, 66 | MatNativeDateModule, 67 | MatPaginatorModule, 68 | MatProgressBarModule, 69 | MatProgressSpinnerModule, 70 | MatRadioModule, 71 | MatRippleModule, 72 | MatSelectModule, 73 | MatSidenavModule, 74 | MatSliderModule, 75 | MatSlideToggleModule, 76 | MatSnackBarModule, 77 | MatSortModule, 78 | MatTableModule, 79 | MatTabsModule, 80 | MatToolbarModule, 81 | MatTooltipModule, 82 | PortalModule, 83 | ScrollingModule, 84 | ], 85 | }) 86 | export class MaterialModule {} 87 | 88 | /** Copyright 2019 Google LLC. All Rights Reserved. 89 | Use of this source code is governed by an MIT-style license that 90 | can be found in the LICENSE file at http://angular.io/license */ 91 | -------------------------------------------------------------------------------- /src/src/app/Components/notifications/notifications.component.css: -------------------------------------------------------------------------------- 1 | 2 | ::ng-deep .mat-line{ 3 | word-wrap: break-word !important; 4 | white-space: pre-wrap !important; 5 | } 6 | 7 | ::ng-deep .mat-list .mat-list-item{ 8 | height:initial!important; 9 | } 10 | 11 | .notification-item-main { 12 | margin-top: 10px; 13 | margin-bottom: 5px; 14 | } 15 | 16 | .notification-item-error { 17 | border-left: 4px solid lightskyblue; 18 | } 19 | 20 | mat-list { 21 | max-height: 450px !important; 22 | overflow-y: auto; 23 | } 24 | 25 | .loading { 26 | margin: 50px; 27 | } 28 | -------------------------------------------------------------------------------- /src/src/app/Components/notifications/notifications.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 9 |
10 |
11 | {{ notification.Message }} 12 | 20 | 21 | 24 | 25 | 26 | 29 |
30 | 31 | 32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | Show Read 40 | 41 |
42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/src/app/Components/notifications/notifications.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NotificationsComponent } from './notifications.component'; 4 | 5 | describe('NotificationsComponent', () => { 6 | let component: NotificationsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NotificationsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NotificationsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Components/notifications/notifications.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Notification, 3 | NotificationService, 4 | } from '../../Services/notification.service'; 5 | import { Component, OnDestroy, OnInit } from '@angular/core'; 6 | import { Subject, zip } from 'rxjs'; 7 | import { takeUntil } from 'rxjs/operators'; 8 | import { NotifierService } from 'angular-notifier'; 9 | 10 | @Component({ 11 | templateUrl: './notifications.component.html', 12 | styleUrls: ['./notifications.component.css'], 13 | selector: 'app-notification-panel', 14 | }) 15 | export class NotificationsComponent implements OnInit, OnDestroy { 16 | destroy$: Subject = new Subject(); 17 | 18 | constructor( 19 | private notificationService: NotificationService, 20 | private notifier: NotifierService 21 | ) {} 22 | 23 | ngOnInit(): void { 24 | this.refreshNotifications(); 25 | this.notificationService.newNotificationReceived 26 | .pipe(takeUntil(this.destroy$)) 27 | .subscribe(() => { 28 | this.refreshNotifications(); 29 | }); 30 | } 31 | 32 | notifications$; 33 | 34 | ngOnDestroy(): void { 35 | this.destroy$.next(true); 36 | this.destroy$.unsubscribe(); 37 | } 38 | 39 | refreshNotifications(): void { 40 | this.notifications$ = this.notificationService.getNotifications(); 41 | } 42 | 43 | updateNotificationReadStatus(notificationId: string, isRead: boolean): void { 44 | this.notificationService 45 | .updateNotificationReadStatus(notificationId, isRead) 46 | .subscribe((result) => { 47 | if (result === false) { 48 | this.notifier.notify( 49 | 'error', 50 | `Failed to mark notification as ${isRead ? 'read' : 'unread'}` 51 | ); 52 | } 53 | this.refreshNotifications(); 54 | this.notificationService.triggerUnreadRefresh(); 55 | }); 56 | } 57 | 58 | markMultipleAsRead(notifications: Notification[]): void { 59 | zip( 60 | ...notifications 61 | .filter((n) => n.MarkedAsRead === false) 62 | .map((n) => 63 | this.notificationService.updateNotificationReadStatus(n.ID, true) 64 | ) 65 | ).subscribe({ 66 | next: (result) => { 67 | if (!result.every((n) => n === true)) { 68 | this.notifier.notify( 69 | 'error', 70 | 'Failed to mark some notifications as read' 71 | ); 72 | } 73 | 74 | this.refreshNotifications(); 75 | this.notificationService.triggerUnreadRefresh(); 76 | }, 77 | }); 78 | } 79 | 80 | deleteNotification(notificationId: string): void { 81 | if ( 82 | confirm('Are you sure you want to delete this notification') === false 83 | ) { 84 | return; 85 | } 86 | 87 | this.notificationService 88 | .deleteNotification(notificationId) 89 | .subscribe((result) => { 90 | if (result === false) { 91 | this.notifier.notify('error', 'Failed to delete notification'); 92 | } 93 | this.refreshNotifications(); 94 | this.notificationService.triggerUnreadRefresh(); 95 | }); 96 | } 97 | 98 | deleteMultipleNotifications(notifications: Notification[]): void { 99 | if ( 100 | confirm('Are you sure you want to delete ALL notifications') === false 101 | ) { 102 | return; 103 | } 104 | 105 | zip( 106 | ...notifications.map((n) => 107 | this.notificationService.deleteNotification(n.ID) 108 | ) 109 | ).subscribe((result) => { 110 | if (!result.every((n) => n === true)) { 111 | this.notifier.notify('error', 'Failed to delete some notifications'); 112 | } 113 | 114 | this.refreshNotifications(); 115 | this.notificationService.triggerUnreadRefresh(); 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/src/app/Components/table-formatters/IconFormatterComponent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: 'app-image-formatter-cell', 5 | template: `` }) 6 | 7 | export class ImageFormatterComponent { 8 | params: any; 9 | agInit(params: any){ 10 | this.params = params; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/src/app/Components/transaction-form/transaction-form.component.css: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | button { 5 | margin-right: 10px; 6 | } 7 | 8 | @media (min-width: 601px) { 9 | .row { 10 | margin-left: 2px; 11 | } 12 | } 13 | 14 | form { 15 | max-width: 1000px; 16 | } 17 | -------------------------------------------------------------------------------- /src/src/app/Components/transaction-form/transaction-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Date 4 | 5 | Date Required 6 | 7 |
8 | 9 |
10 | 11 | Account 12 | 17 | 18 | {{ account.AccountName }} 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | Amount 29 | 30 | Amount Required 31 | 32 |
33 | 34 | 40 | 41 | 42 | Status 43 | 44 | Settled 45 | Pending 46 | 47 | 48 |
49 | 50 | 51 | Type 52 | 53 | 54 |
55 | 56 | 57 | Vendor 58 | 59 | 60 |
61 | 62 | 63 | Category 64 | 65 | 66 |
67 | 68 | 69 | Merchant 70 | 71 | 72 |
73 | 74 | 75 | Note 76 | 77 | 78 |
79 | 80 | 81 | 82 |
83 | 92 | 95 |
96 |
97 | -------------------------------------------------------------------------------- /src/src/app/Components/transaction-form/transaction-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TransactionFormComponent } from './transaction-form.component'; 4 | 5 | describe('TransactionFormComponent', () => { 6 | let component: TransactionFormComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TransactionFormComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TransactionFormComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Components/transaction-form/transaction-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core'; 2 | import { FormGroup, FormControl, Validators } from '@angular/forms'; 3 | import { DatePipe } from '@angular/common'; 4 | import { Router } from '@angular/router'; 5 | import { _MatTabGroupBase } from '@angular/material/tabs'; 6 | import { AccountsService } from 'src/app/Services/accounts.service'; 7 | import { map } from 'rxjs/operators'; 8 | 9 | @Component({ 10 | selector: 'app-transaction-form', 11 | templateUrl: './transaction-form.component.html', 12 | styleUrls: ['./transaction-form.component.css'], 13 | }) 14 | export class TransactionFormComponent implements OnInit { 15 | constructor( 16 | private datePipe: DatePipe, 17 | private router: Router, 18 | private accountsService: AccountsService 19 | ) {} 20 | 21 | @Input() hideAccountSelector: string = 'false'; 22 | @Input() parentValid: boolean = true; 23 | @Output() save: EventEmitter = new EventEmitter(); 24 | 25 | transactionForm = new FormGroup({ 26 | date: new FormControl(null, [Validators.required]), 27 | account: new FormControl(null, [Validators.required]), 28 | amount: new FormControl(null, [Validators.required]), 29 | currency: new FormControl('GBP'), 30 | status: new FormControl('SETTLED', [Validators.required]), 31 | type: new FormControl(null), 32 | vendor: new FormControl(null), 33 | category: new FormControl(null), 34 | merchant: new FormControl(null), 35 | note: new FormControl(null), 36 | }); 37 | 38 | accounts$ = this.accountsService.getAccounts().pipe( 39 | map((accounts) => 40 | accounts.map((a) => { 41 | return { AccountId: a.ID, AccountName: a.AccountName }; 42 | }) 43 | ) 44 | ); 45 | 46 | ngOnInit(): void { 47 | if (this.hideAccountSelector == 'true') { 48 | this.transactionForm.controls.account.clearValidators(); 49 | } 50 | } 51 | 52 | setFormValues(transaction: any) { 53 | try { 54 | this.transactionForm.controls.date.setValue( 55 | this.datePipe.transform(transaction.Date, 'yyyy-MM-dd') 56 | ); 57 | this.transactionForm.controls.account.setValue({ 58 | AccountId: transaction.AccountID, 59 | AccountName: transaction.AccountName, 60 | }); 61 | this.transactionForm.controls.amount.setValue(transaction.Amount); 62 | this.transactionForm.controls.currency.setValue(transaction.Currency); 63 | this.transactionForm.controls.status.setValue(transaction.Status); 64 | this.transactionForm.controls.type.setValue(transaction.Type); 65 | this.transactionForm.controls.vendor.setValue(transaction.Vendor); 66 | this.transactionForm.controls.category.setValue(transaction.Category); 67 | this.transactionForm.controls.merchant.setValue(transaction.Merchant); 68 | this.transactionForm.controls.note.setValue(transaction.Note); 69 | } catch (ex) { 70 | console.log(ex); 71 | } 72 | } 73 | 74 | enable() { 75 | this.transactionForm.enable(); 76 | } 77 | 78 | disable() { 79 | this.transactionForm.disable(); 80 | } 81 | 82 | submit() { 83 | this.disable(); 84 | this.save.emit(this.transactionForm); 85 | } 86 | 87 | cancel() { 88 | if (window.history.length > 0) { 89 | window.history.back(); 90 | } else { 91 | this.router.navigate(['/transactions']); 92 | } 93 | } 94 | 95 | compareAccounts(a1, a2) { 96 | return accountComparer(a1, a2); 97 | } 98 | } 99 | 100 | export function accountComparer(a1, a2) { 101 | if (!a1 || !a2) { 102 | return false; 103 | } 104 | return a1.AccountName == a2.AccountName && a1.AccountId == a2.AccountId; 105 | } 106 | -------------------------------------------------------------------------------- /src/src/app/Components/widgets/balance-history-chart/balance-history-chart.component.css: -------------------------------------------------------------------------------- 1 | .rangeDropdown { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | margin-top: 5px; 6 | margin-right: 15px; 7 | } 8 | 9 | @media (max-width: 1100px) { 10 | .rangeDropdown { 11 | position: relative; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/src/app/Components/widgets/balance-history-chart/balance-history-chart.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
Balance History
4 | 5 | 6 | Year 7 | 6 Months 8 | Quarter 9 | Month 10 | Week 11 | 12 | 13 |
14 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /src/src/app/Components/widgets/balance-history-chart/balance-history-chart.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BalanceHistoryChartComponent } from './balance-history-chart.component'; 4 | 5 | describe('BalanceHistoryChartComponent', () => { 6 | let component: BalanceHistoryChartComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ BalanceHistoryChartComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(BalanceHistoryChartComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Components/widgets/spent-per-category-chart/spent-per-category-chart.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/app/Components/widgets/spent-per-category-chart/spent-per-category-chart.component.css -------------------------------------------------------------------------------- /src/src/app/Components/widgets/spent-per-category-chart/spent-per-category-chart.component.html: -------------------------------------------------------------------------------- 1 | 2 |
Spent per Category this Month
3 |
4 | 5 |
6 | -------------------------------------------------------------------------------- /src/src/app/Components/widgets/spent-per-category-chart/spent-per-category-chart.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SpentPerCategoryChartComponent } from './spent-per-category-chart.component'; 4 | 5 | describe('SpentPerCategoryChartComponent', () => { 6 | let component: SpentPerCategoryChartComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SpentPerCategoryChartComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SpentPerCategoryChartComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Components/widgets/spent-per-category-chart/spent-per-category-chart.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { StatisticsService } from 'src/app/Services/statistics.service'; 3 | import { PieChart } from '../../../Models/pie.chart'; 4 | import * as Chart from 'chart.js'; 5 | import 'chartjs-plugin-colorschemes'; 6 | import { MatDialog } from '@angular/material/dialog'; 7 | import { CurrencyPipe } from '@angular/common'; 8 | 9 | @Component({ 10 | selector: 'app-spent-per-category-chart', 11 | templateUrl: './spent-per-category-chart.component.html', 12 | styleUrls: ['./spent-per-category-chart.component.css'], 13 | }) 14 | export class SpentPerCategoryChartComponent implements OnInit, OnDestroy { 15 | constructor( 16 | private statisticsService: StatisticsService, 17 | private dialog: MatDialog, 18 | private currencyPipe: CurrencyPipe 19 | ) {} 20 | 21 | isMobile: boolean = true; 22 | hasLoaded: boolean = false; 23 | chart; 24 | 25 | ngOnInit(): void { 26 | this.loadData(); 27 | } 28 | 29 | loadData(): void { 30 | this.statisticsService.getSpentAmountPerCategory().subscribe({ 31 | next: (data) => this.buildChart(data), 32 | }); 33 | } 34 | 35 | buildChart(data: []): void { 36 | const chartConfig = new PieChart(); 37 | 38 | const dataset = { 39 | fill: false, 40 | borderWidth: 2, 41 | data: Object.values(data), 42 | }; 43 | chartConfig.data.datasets.push(dataset); 44 | chartConfig.data.labels = Object.keys(data); 45 | 46 | chartConfig.options.tooltips = { 47 | callbacks: { 48 | label: (tooltipItems, data) => 49 | ` ${data.labels[tooltipItems.index]} ${this.currencyPipe.transform( 50 | data.datasets[0].data[tooltipItems.index] 51 | )}`, 52 | }, 53 | }; 54 | 55 | const spentPerCategoryChart = document.getElementById( 56 | 'spentPerCategoryChart' 57 | ) as HTMLCanvasElement; 58 | 59 | this.chart = new Chart(spentPerCategoryChart.getContext('2d'), chartConfig); 60 | 61 | // this.chart.options.responsive = this.chart.options.legend.display = this.chart.options.maintainAspectRatio = !this 62 | // .isMobile; 63 | 64 | this.chart.options.maintainAspectRatio = false; 65 | this.chart.options.responsive = true; 66 | 67 | this.chart.update(); 68 | 69 | this.hasLoaded = true; 70 | } 71 | 72 | ngOnDestroy() { 73 | this.chart.destroy(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/src/app/Guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | RouterStateSnapshot, 6 | Router, 7 | } from '@angular/router'; 8 | import { FinanceApiRequest } from '../Services/finance-api.request.service'; 9 | import * as jwt_decode from 'jwt-decode'; 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class AuthGuard implements CanActivate { 13 | constructor(private router: Router) {} 14 | 15 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 16 | if (FinanceApiRequest.Token) { 17 | const token = jwt_decode(FinanceApiRequest.Token); 18 | const expirary = new Date(token.exp * 1000); 19 | if (expirary < new Date()) { 20 | return this.denyAccess(state.url); 21 | } 22 | 23 | return true; 24 | } 25 | return this.denyAccess(state.url); 26 | } 27 | 28 | canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 29 | return this.canActivate(route, state); 30 | } 31 | 32 | denyAccess(redirectUrl: string): boolean { 33 | if (redirectUrl === '/') { 34 | redirectUrl = null; 35 | } 36 | 37 | this.router.navigate(['login'], { queryParams: { redirect: redirectUrl } }); 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/src/app/Guards/demo.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | RouterStateSnapshot, 6 | Router, 7 | } from '@angular/router'; 8 | import { ConfigService } from '../Services/config.service'; 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class DemoGuard implements CanActivate { 12 | constructor(private configService: ConfigService, private router: Router) {} 13 | 14 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 15 | return this.configService.getValue('IsDemo').then((isDemo) => { 16 | if (isDemo === true) { 17 | console.error('Route is not allowed in demo mode'); 18 | this.router.navigate(['/404']); 19 | return false; 20 | } 21 | 22 | return true; 23 | }); 24 | } 25 | 26 | canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 27 | return this.canActivate(route, state); 28 | } 29 | 30 | private readonly demoRouteExclusionList: string[] = ['']; 31 | } 32 | -------------------------------------------------------------------------------- /src/src/app/Models/account-settings.ts: -------------------------------------------------------------------------------- 1 | export interface AccountSettings { 2 | AccountID: string; 3 | RefreshInterval: RefreshIntervals 4 | GenerateAdjustments: boolean; 5 | NotifyAccountRefreshes: boolean; 6 | } 7 | 8 | export enum RefreshIntervals { 9 | Never = "Never", 10 | hourly = 'hourly', 11 | sixHours = 'sixHours', 12 | biDaily = 'biDaily', 13 | Daily = 'Daily', 14 | } 15 | -------------------------------------------------------------------------------- /src/src/app/Models/account.model.ts: -------------------------------------------------------------------------------- 1 | export interface Account { 2 | ID: string; 3 | ClientID: string; 4 | AccountName: string; 5 | CurrentBalance: number; 6 | AvailableBalance: number; 7 | LastRefreshed: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/src/app/Models/asset.model.ts: -------------------------------------------------------------------------------- 1 | export interface Asset { 2 | Id: string; 3 | Name: string; 4 | ClientId: string; 5 | Source: string; 6 | Type: 'Unknown' | 'Crypto' | 'Stock' | 'Cash' | 'Fiat'; 7 | Code: string; 8 | Exchange?: string; 9 | Currency: string; 10 | Balance: number; 11 | MarketIdentifiers?: any; 12 | } 13 | -------------------------------------------------------------------------------- /src/src/app/Models/line.chart.ts: -------------------------------------------------------------------------------- 1 | export class LineChart { 2 | type = 'line'; 3 | data = { 4 | labels: [], 5 | datasets: [], 6 | }; 7 | options: any = { 8 | scales: { 9 | xAxes: [ 10 | { 11 | type: 'time', 12 | time: { 13 | unit: 'month', 14 | }, 15 | }, 16 | ], 17 | }, 18 | responsive: true, 19 | plugins: { 20 | colorschemes: { scheme: 'brewer.Dark2-8' }, 21 | zoom: { 22 | zoom: { 23 | enabled: true, 24 | mode: 'x', 25 | }, 26 | }, 27 | }, 28 | tooltips: { 29 | intersect: false, 30 | mode: 'index', 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/src/app/Models/pie.chart.ts: -------------------------------------------------------------------------------- 1 | export class PieChart { 2 | type = 'pie'; 3 | data = { 4 | labels: [], 5 | datasets: [], 6 | }; 7 | options = { 8 | responsive: true, 9 | plugins: { 10 | colorschemes: { 11 | scheme: 'brewer.Dark2-7', 12 | }, 13 | }, 14 | tooltips: {}, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/src/app/Models/trade.model.ts: -------------------------------------------------------------------------------- 1 | export interface Trade { 2 | Id: string; 3 | AssetId: string; 4 | TradeDateTime: Date; 5 | Amount: number; 6 | Currency: string; 7 | Description: string; 8 | Status: string; 9 | Type?: "Sale" | "Buy"; 10 | Note?: string; 11 | Source: string; 12 | Owner: string; 13 | ExtraDetails: any; 14 | } 15 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-assets/wealth-assets.component.css: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-assets/wealth-assets.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 |
Name 5 | {{asset.Name}} 6 | Code 12 | {{getAssetCode(asset)}} 13 | Balance 19 | {{asset.Balance | number:'1.2-16'}} 20 | Currency 26 | {{asset.Currency}} 27 | Type 33 | {{asset.Type}} 34 | Source 40 | {{asset.Source | titlecase}} 41 | Last Updated 47 | {{asset.LastUpdated | date: 'medium'}} 48 |
54 | 55 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-assets/wealth-assets.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { WealthAssetsComponent } from './wealth-assets.component'; 4 | 5 | describe('WealthAssetsComponent', () => { 6 | let component: WealthAssetsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ WealthAssetsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(WealthAssetsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-assets/wealth-assets.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Asset } from 'src/app/Models/asset.model'; 3 | import { AssetsService } from 'src/app/Services/wealth/assets.service'; 4 | 5 | @Component({ 6 | templateUrl: './wealth-assets.component.html', 7 | styleUrls: ['./wealth-assets.component.css'] 8 | }) 9 | export class WealthAssetsComponent { 10 | 11 | constructor(private assetsService: AssetsService) { } 12 | 13 | assets = this.assetsService.getAssets(); 14 | displayedColumns = ["name", "code", "balance", "currency", "type", "source", "updated"] 15 | 16 | getAssetCode(asset: Asset){ 17 | if (asset.Exchange) { 18 | return asset.Code + " | " + asset.Exchange; 19 | } 20 | 21 | return asset.Code; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-portfolio/wealth-portfolio.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/app/Modules/wealth/Pages/wealth-portfolio/wealth-portfolio.component.css -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-portfolio/wealth-portfolio.component.html: -------------------------------------------------------------------------------- 1 |

Work in Progress 👷‍♂️🚧

2 | 3 |

This page is a work in progress, to see wealth info see assets page or the trades page

4 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-portfolio/wealth-portfolio.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { WealthPortfolioComponent } from './wealth-portfolio.component'; 4 | 5 | describe('WealthPortfolioComponent', () => { 6 | let component: WealthPortfolioComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ WealthPortfolioComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(WealthPortfolioComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-portfolio/wealth-portfolio.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './wealth-portfolio.component.html', 5 | styleUrls: ['./wealth-portfolio.component.css'] 6 | }) 7 | export class WealthPortfolioComponent implements OnInit { 8 | 9 | constructor() { } 10 | 11 | ngOnInit(): void { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-trades/wealth-trades.component.css: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-trades/wealth-trades.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 61 |
DateTime 5 | {{trade.TradeDateTime | date: 'dd/MM/yyyy HH:mm:ss'}} 6 | Asset 12 | {{trade.AssetName}} 13 | Amount 19 | {{trade.Amount | number:'1.2-16'}} 20 | Currency 26 | {{trade.Currency}} 27 | Description 33 | {{trade.Description}} 34 | Status 41 | {{trade.Status | titlecase}} 42 | Source 48 | {{trade.Source | titlecase}} 49 | Type 55 | {{trade.Type}} 56 |
62 | 68 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-trades/wealth-trades.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { WealthTradesComponent } from './wealth-trades.component'; 4 | 5 | describe('WealthTradesComponent', () => { 6 | let component: WealthTradesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ WealthTradesComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(WealthTradesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/Pages/wealth-trades/wealth-trades.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { MatPaginator } from '@angular/material/paginator'; 3 | import { MatSort } from '@angular/material/sort'; 4 | import { MatTableDataSource } from '@angular/material/table'; 5 | import { zip } from 'rxjs'; 6 | import { map } from 'rxjs/operators'; 7 | import { Trade } from 'src/app/Models/trade.model'; 8 | import { AssetsService } from 'src/app/Services/wealth/assets.service'; 9 | import { TradesService } from 'src/app/Services/wealth/trades.service'; 10 | 11 | @Component({ 12 | templateUrl: './wealth-trades.component.html', 13 | styleUrls: ['./wealth-trades.component.css'], 14 | }) 15 | export class WealthTradesComponent { 16 | constructor( 17 | private tradeService: TradesService, 18 | private assetService: AssetsService 19 | ) {} 20 | 21 | @ViewChild(MatSort, { static: true }) sort: MatSort; 22 | @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; 23 | displayedColumns = [ 24 | 'TradeDateTime', 25 | 'AssetName', 26 | 'Amount', 27 | 'Currency', 28 | 'Description', 29 | 'Status', 30 | 'Source', 31 | 'Type', 32 | ]; 33 | 34 | data = zip(this.tradeService.getTrades(), this.assetService.getAssets()).pipe( 35 | map(([trades, assets]) => { 36 | const combined = trades.map((t) => { 37 | const te = t as TradeExtra; 38 | te.AssetName = assets.find((a) => a.Id === t.AssetId).Name; 39 | return te; 40 | }); 41 | const table = new MatTableDataSource(combined); 42 | table.sort = this.sort; 43 | table.paginator = this.paginator; 44 | return table; 45 | }) 46 | ); 47 | 48 | scrollToTop() { 49 | const matTable = document.getElementById('trade-table'); 50 | matTable.scrollTop = 0; 51 | } 52 | } 53 | 54 | interface TradeExtra extends Trade { 55 | AssetName: string; 56 | } 57 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/wealth-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { WealthPortfolioComponent } from './Pages/wealth-portfolio/wealth-portfolio.component'; 5 | import { WealthAssetsComponent } from './Pages/wealth-assets/wealth-assets.component'; 6 | import { WealthTradesComponent } from './Pages/wealth-trades/wealth-trades.component'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: 'portfolio', 11 | component: WealthPortfolioComponent, 12 | data: { title: 'Portfolio' }, 13 | }, 14 | { 15 | path: 'assets', 16 | component: WealthAssetsComponent, 17 | data: { title: 'Assets' }, 18 | }, 19 | { 20 | path: 'trades', 21 | component: WealthTradesComponent, 22 | data: { title: 'Trades' }, 23 | }, 24 | ]; 25 | 26 | @NgModule({ 27 | imports: [RouterModule.forChild(routes)], 28 | exports: [RouterModule], 29 | }) 30 | export class WealthRoutingModule {} 31 | -------------------------------------------------------------------------------- /src/src/app/Modules/wealth/wealth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { WealthRoutingModule } from './wealth-routing.module'; 5 | import { WealthPortfolioComponent } from './Pages/wealth-portfolio/wealth-portfolio.component'; 6 | import { WealthAssetsComponent } from './Pages/wealth-assets/wealth-assets.component'; 7 | import { WealthTradesComponent } from './Pages/wealth-trades/wealth-trades.component'; 8 | import { ComponentModule } from 'src/app/Components/component.module'; 9 | import { MaterialModule } from 'src/app/Components/material.module'; 10 | 11 | 12 | @NgModule({ 13 | declarations: [WealthPortfolioComponent, WealthAssetsComponent, WealthTradesComponent], 14 | imports: [ 15 | CommonModule, 16 | WealthRoutingModule, 17 | ComponentModule, 18 | MaterialModule 19 | ] 20 | }) 21 | export class WealthModule { } 22 | -------------------------------------------------------------------------------- /src/src/app/Navigation/MenuItems.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from "rxjs"; 2 | import { map } from "rxjs/operators"; 3 | import { ConfigService } from "../Services/config.service"; 4 | 5 | export const MenuItems: (MenuItem | MenuItemDivider)[] = [ 6 | { 7 | type: 'page', 8 | name: 'Dashboard', 9 | icon: 'fas fa-home', 10 | route: '/', 11 | routerLinkActiveOptions: { exact: true }, 12 | }, 13 | { 14 | type: 'page', 15 | name: 'Accounts', 16 | icon: 'fas fa-user', 17 | route: '/accounts', 18 | }, 19 | { 20 | type: 'page', 21 | name: 'Transactions', 22 | icon: 'fas fa-money-check-alt', 23 | route: '/transactions', 24 | }, 25 | // { 26 | // name: 'Budgets', 27 | // icon: 'fas fa-funnel-dollar', 28 | // route: '/budgets', 29 | // matTooltip: 'Future Feature to be Implemented', 30 | // }, 31 | { 32 | type: 'page', 33 | name: 'Goals', 34 | icon: 'fas fa-balance-scale', 35 | route: '/goals', 36 | }, 37 | { 38 | type: 'divider', 39 | }, 40 | { 41 | type: 'page', 42 | name: 'Portfolio', 43 | icon : 'fas fa-chart-pie', 44 | route: '/wealth/portfolio', 45 | show: (c) => c.getClientValue('enable_wealth').pipe(map(v => v !== 'false')), 46 | }, 47 | { 48 | type: 'page', 49 | name: 'Assets', 50 | icon: 'fas fa-coins', 51 | route: '/wealth/assets', 52 | show: (c) => c.getClientValue('enable_wealth').pipe(map(v => v !== 'false')), 53 | }, 54 | { 55 | type: 'page', 56 | name: 'Trades', 57 | icon: 'fas fa-receipt', 58 | route: '/wealth/trades', 59 | show: (c) => c.getClientValue('enable_wealth').pipe(map(v => v !== 'false')), 60 | }, 61 | // { 62 | // type: 'page', 63 | // name: 'Performance', 64 | // icon: 'fas fa-chart-line', 65 | // route: '/wealth/performance', 66 | // }, 67 | { 68 | type: 'divider' 69 | }, 70 | { 71 | type: 'page', 72 | name: 'Datafeeds', 73 | icon: 'fas fa-rss', 74 | route: '/datafeeds', 75 | }, 76 | ]; 77 | 78 | interface MenuItem { 79 | name: string; 80 | type: 'page'; 81 | icon: string; 82 | route: string; 83 | routerLinkActiveOptions?: { exact: boolean }; 84 | matTooltip?: string; 85 | show?: (c: ConfigService) => Observable; 86 | } 87 | 88 | interface MenuItemDivider { 89 | type: 'divider'; 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/src/app/Navigation/navigation.component.css: -------------------------------------------------------------------------------- 1 | .box-shadow { 2 | box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.05); 3 | } 4 | 5 | .sidenav { 6 | width: 300px; 7 | border: 0; 8 | } 9 | 10 | .mat-toolbar.mat-primary { 11 | position: sticky; 12 | top: 0; 13 | z-index: 10; 14 | } 15 | 16 | .container-fluid { 17 | margin-top: 15px; 18 | margin-bottom: 10px; 19 | } 20 | 21 | .sidenav-container { 22 | margin-top: 55px; 23 | } 24 | 25 | .mat-toolbar, 26 | #top-bar { 27 | width: 100%; 28 | z-index: 2; 29 | } 30 | 31 | mat-sidenav-content { 32 | border: 0; 33 | } 34 | 35 | /*Pads Icons*/ 36 | .fas, 37 | .far { 38 | padding-right: 10px; 39 | } 40 | 41 | .fill-remaining-space { 42 | /* This fills the remaining space, by using flexbox. 43 | Every toolbar row uses a flexbox row layout. */ 44 | flex: 1 1 auto; 45 | } 46 | 47 | #tool-menu { 48 | vertical-align: middle; 49 | } 50 | 51 | #loading-bar { 52 | z-index: 10; 53 | position: absolute; 54 | } 55 | 56 | mat-toolbar { 57 | max-height: 60px; 58 | } 59 | 60 | .short-text { 61 | display: none; 62 | } 63 | @media (max-width: 385px) { 64 | .short-text { 65 | display: inline-block; 66 | } 67 | .full-text { 68 | display: none; 69 | } 70 | } 71 | 72 | .menu-item { 73 | border-top-right-radius: 24px; 74 | border-bottom-right-radius: 24px; 75 | } 76 | 77 | ::ng-deep.notification-menu { 78 | max-width: 100vw !important; 79 | width: 450px; 80 | } 81 | -------------------------------------------------------------------------------- /src/src/app/Navigation/navigation.component.ts: -------------------------------------------------------------------------------- 1 | import { NotificationService } from './../Services/notification.service'; 2 | import { Component, ViewChild } from '@angular/core'; 3 | import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; 4 | import { Observable } from 'rxjs'; 5 | import { map, shareReplay } from 'rxjs/operators'; 6 | import { MatSidenav } from '@angular/material/sidenav'; 7 | import { FinanceApiRequest } from '../Services/finance-api.request.service'; 8 | import { Router } from '@angular/router'; 9 | import { TitleService } from '../Services/title.service'; 10 | import { MenuItems } from './MenuItems'; 11 | import { ConfigService } from '../Services/config.service'; 12 | 13 | @Component({ 14 | selector: 'app-navigation', 15 | templateUrl: './navigation.component.html', 16 | styleUrls: ['./navigation.component.css'], 17 | }) 18 | export class NavigationComponent { 19 | isExpanded = false; 20 | IsMobile = false; 21 | LoadingBar = false; 22 | 23 | constructor( 24 | private breakpointObserver: BreakpointObserver, 25 | private router: Router, 26 | private titleService: TitleService, 27 | public configService: ConfigService, 28 | private notificationService: NotificationService 29 | ) {} 30 | @ViewChild(MatSidenav, { static: false }) public sidenav: MatSidenav; 31 | 32 | isHandset$: Observable = this.breakpointObserver 33 | .observe(Breakpoints.Handset) 34 | .pipe( 35 | map((result) => result.matches), 36 | shareReplay(), 37 | map((isHandset) => (this.IsMobile = isHandset)) 38 | ); 39 | 40 | page_title$ = this.titleService.title; 41 | showBackButton$ = this.titleService.showBackButton; 42 | menuItems = MenuItems; 43 | siteName = ''; 44 | notificationCount = this.notificationService.getUnreadPoll(); 45 | 46 | ngOnInit(): void { 47 | this.configService 48 | .getValue('SiteName') 49 | .then((name) => (this.siteName = name ?? 'Finance Manager')); 50 | } 51 | 52 | PageChanged(): void { 53 | if (this.IsMobile === true) { 54 | this.sidenav.close(); 55 | } 56 | } 57 | 58 | LogOut(): void { 59 | FinanceApiRequest.setToken(null); 60 | this.router.navigate(['login']); 61 | } 62 | 63 | goBack() { 64 | window.history.back(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/src/app/Pages/account-details/account-details-external-accounts/account-details-external-accounts.component.css: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100%; 3 | } 4 | 5 | #settingsForm { 6 | margin-bottom: 15px; 7 | } 8 | -------------------------------------------------------------------------------- /src/src/app/Pages/account-details/account-details-external-accounts/account-details-external-accounts.component.html: -------------------------------------------------------------------------------- 1 |

External Account Settings

2 |
3 | 4 | Auto Refresh Interval 5 | 6 | Never 7 | Hourly 8 | 6 Hours 9 | 12 Hours 10 | Daily 11 | 12 | 13 |
14 | 15 | Generate Adjustment Transactions
18 | 19 | Notify on Account Refresh 22 |
23 | 24 |

Link External Account

25 | Link your account with an external datafeed account so that you can 27 | automatically sync transactions to this account 29 | 30 |
31 |
32 |

{{ account?.AccountName }}

33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 58 | 78 | 79 | 80 | 81 | 82 |
Provider 38 | {{ externalAccount.Provider }} 39 | Vendor 45 | {{ externalAccount.VendorName }} 46 | Name 52 | {{ externalAccount.AccountName }} 53 | 59 | 68 | 77 |
83 | -------------------------------------------------------------------------------- /src/src/app/Pages/account-details/account-details-external-accounts/account-details-external-accounts.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AccountDetailsExternalAccountsComponent } from './account-details-external-accounts.component'; 4 | 5 | describe('AccountDetailsExternalAccountsComponent', () => { 6 | let component: AccountDetailsExternalAccountsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AccountDetailsExternalAccountsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AccountDetailsExternalAccountsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/account-details/account-details.component.css: -------------------------------------------------------------------------------- 1 | .current-balance { 2 | border-bottom: 4px solid lightgreen; 3 | } 4 | 5 | .available-balance { 6 | border-bottom: 4px solid lightskyblue; 7 | } 8 | 9 | .spent-this-week { 10 | border-bottom: 4px solid lightcoral; 11 | } 12 | 13 | .top-categories { 14 | border-bottom: 4px solid purple; 15 | } 16 | 17 | .transactions-section { 18 | margin-top: 25px; 19 | } 20 | -------------------------------------------------------------------------------- /src/src/app/Pages/account-details/account-details.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Last Refreshed: {{ account.LastRefreshed | date: "HH:mm dd MMMM" }}
6 | 15 | 23 | 26 | 29 |
30 |
36 | 37 | Current Balance 38 | {{ 39 | account.CurrentBalance | currency: account.BaseCurrency 40 | }} 41 | 42 | 43 | Available Balance 44 | {{ 45 | account.AvailableBalance | currency: account.BaseCurrency 46 | }} 47 | 48 | 49 | Spent This Week 50 | {{ 51 | spentThisWeek | currency: account.BaseCurrency 52 | }} 53 | 56 | 57 |
58 | 59 |

Account Transactions:

60 | 61 |
62 | 63 |

Could not Find Account

64 | -------------------------------------------------------------------------------- /src/src/app/Pages/account-details/account-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AccountDetailsComponent } from './account-details.component'; 4 | 5 | describe('AccountDetailsComponent', () => { 6 | let component: AccountDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AccountDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AccountDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/account-details/account-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AccountsService } from 'src/app/Services/accounts.service'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | import { catchError, shareReplay, tap } from 'rxjs/operators'; 5 | import { DatafeedsService } from 'src/app/Services/datafeeds.service'; 6 | import { IsLoadingService } from '@service-work/is-loading'; 7 | import { of, Observable } from 'rxjs'; 8 | import { TitleService } from 'src/app/Services/title.service'; 9 | import { NotifierService } from 'angular-notifier'; 10 | import { ConfigService } from 'src/app/Services/config.service'; 11 | import { Account } from 'src/app/Models/account.model'; 12 | 13 | @Component({ 14 | templateUrl: './account-details.component.html', 15 | styleUrls: ['./account-details.component.css'], 16 | }) 17 | export class AccountDetailsComponent implements OnInit { 18 | id: string = this.route.snapshot.paramMap.get('id'); 19 | account$: Observable = this.accountsService 20 | .getAccountById(this.id) 21 | .pipe(shareReplay(1)) 22 | .pipe( 23 | tap((a) => setTimeout(() => this.titleService.setTitle(a.AccountName), 0)) 24 | ) 25 | .pipe( 26 | catchError(() => { 27 | this.accountNotFound = true; 28 | return of(null); 29 | }) 30 | ); 31 | 32 | spentThisWeek: number; 33 | 34 | isAccountMapped = this.datafeedsService 35 | .doesAccountHaveExternalMappings(this.id) 36 | .pipe(shareReplay(1)); 37 | 38 | accountNotFound = false; 39 | 40 | isRefreshEnabledLoading = this.loadingService.isLoading$({ 41 | key: ['default', 'refresh-account'], 42 | }); 43 | 44 | constructor( 45 | private accountsService: AccountsService, 46 | private route: ActivatedRoute, 47 | private router: Router, 48 | private datafeedsService: DatafeedsService, 49 | private loadingService: IsLoadingService, 50 | private titleService: TitleService, 51 | private notifier: NotifierService, 52 | private configService: ConfigService 53 | ) { 54 | this.titleService.showBackButton.next(true); 55 | } 56 | 57 | ngOnInit(): void { 58 | this.accountsService 59 | .getSpentThisWeek(this.id) 60 | .pipe( 61 | catchError((ex) => { 62 | console.log(ex); 63 | return of(0); 64 | }) 65 | ) 66 | .subscribe((v) => (this.spentThisWeek = v ?? 0)); 67 | } 68 | 69 | refreshAccount(): void { 70 | if ( 71 | confirm( 72 | 'Are you sure you want to refresh this account.\nThis will fetch the latest data from your datafeeds' 73 | ) 74 | ) { 75 | this.loadingService.add({ key: ['default', 'refresh-account'] }); 76 | this.datafeedsService.refreshAccount(this.id).subscribe({ 77 | next: () => alert('Account will refresh in the background'), 78 | error: (ex) => { 79 | alert('Something went wrong\nCheck the console for more info'); 80 | console.log(ex); 81 | }, 82 | complete: () => 83 | this.loadingService.remove({ key: ['default', 'refresh-account'] }), 84 | }); 85 | } 86 | } 87 | 88 | async delete(accountId: string): Promise { 89 | const isDemo = await this.configService.getValue('IsDemo'); 90 | if (isDemo === true) { 91 | this.notifier.notify('error', 'Cannot delete account in demo mode'); 92 | return; 93 | } 94 | 95 | if ( 96 | confirm( 97 | 'Are you sure you want to delete this account.\nIt will delete all associated transactions' 98 | ) 99 | ) { 100 | this.accountsService 101 | .deleteAccount(accountId) 102 | .subscribe(() => this.router.navigate(['/accounts'])); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/src/app/Pages/accounts/accounts.component.css: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/src/app/Pages/accounts/accounts.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | 29 |
Name 5 | {{ account.AccountName }} 6 | Current Balance 12 | {{ 13 | account.CurrentBalance | currency: account.BaseCurrency 14 | }} 15 | Available Balance 21 | {{ 22 | account.AvailableBalance | currency: account.BaseCurrency 23 | }} 24 |
30 | -------------------------------------------------------------------------------- /src/src/app/Pages/accounts/accounts.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AccountsComponent } from './accounts.component'; 4 | 5 | describe('AccountsComponent', () => { 6 | let component: AccountsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AccountsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AccountsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/accounts/accounts.component.ts: -------------------------------------------------------------------------------- 1 | import { TitleService } from 'src/app/Services/title.service'; 2 | import { Account } from './../../Models/account.model'; 3 | import { Component, OnInit } from '@angular/core'; 4 | import { AccountsService } from '../../Services/accounts.service'; 5 | import { IsLoadingService } from '@service-work/is-loading'; 6 | import { tap } from 'rxjs/operators'; 7 | import { Observable } from 'rxjs'; 8 | 9 | @Component({ 10 | templateUrl: './accounts.component.html', 11 | styleUrls: ['./accounts.component.css'], 12 | }) 13 | export class AccountsComponent implements OnInit { 14 | constructor( 15 | private accountsService: AccountsService, 16 | private loadingService: IsLoadingService, 17 | private titleService: TitleService 18 | ) { 19 | this.titleService.showBackButton.next(false); 20 | } 21 | 22 | accounts: Observable = this.accountsService 23 | .getAccounts() 24 | .pipe( 25 | tap(() => 26 | this.loadingService.remove({ key: ['default', 'accounts-table'] }) 27 | ) 28 | ); 29 | 30 | displayedColumns: string[] = ['name', 'currentbalance', 'availablebalance']; 31 | 32 | ngOnInit(): void { 33 | this.loadingService.add({ key: ['default', 'accounts-table'] }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-account/add-account.component.css: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | button { 5 | margin-right: 10px; 6 | } 7 | 8 | @media (min-width: 601px) { 9 | .row { 10 | margin-left: 2px; 11 | } 12 | } 13 | 14 | form { 15 | max-width: 1000px; 16 | } 17 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-account/add-account.component.html: -------------------------------------------------------------------------------- 1 |

Add Account

2 |
3 | 4 | Name 5 | 6 | Name Required 7 | 8 |
9 | 10 |
11 | 20 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-account/add-account.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddAccountComponent } from './add-account.component'; 4 | 5 | describe('AddAccountComponent', () => { 6 | let component: AddAccountComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AddAccountComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AddAccountComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-account/add-account.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormGroup, FormControl, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { AccountsService } from 'src/app/Services/accounts.service'; 5 | 6 | @Component({ 7 | selector: 'app-add-account', 8 | templateUrl: './add-account.component.html', 9 | styleUrls: ['./add-account.component.css'], 10 | }) 11 | export class AddAccountComponent implements OnInit { 12 | constructor( 13 | private router: Router, 14 | private accountsService: AccountsService 15 | ) {} 16 | 17 | accountForm = new FormGroup({ 18 | accountName: new FormControl(null, [Validators.required]), 19 | }); 20 | 21 | ngOnInit(): void {} 22 | 23 | save() { 24 | const account = { 25 | AccountName: this.accountForm.controls.accountName.value, 26 | }; 27 | this.accountsService.addNewAccount(account).subscribe({ 28 | next: () => this.router.navigate(['/accounts']), 29 | error: (err) => console.log(err), 30 | }); 31 | } 32 | 33 | cancel() { 34 | if (window.history.length > 0) { 35 | window.history.back(); 36 | } else { 37 | this.router.navigate(['/accounts']); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-split-transaction/add-split-transaction.component.css: -------------------------------------------------------------------------------- 1 | mat-form-field, 2 | table { 3 | width: 100%; 4 | } 5 | button { 6 | margin-right: 10px; 7 | } 8 | 9 | @media (min-width: 601px) { 10 | .row { 11 | margin-left: 2px; 12 | } 13 | } 14 | 15 | form { 16 | max-width: 1000px; 17 | } 18 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-split-transaction/add-split-transaction.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Add Split Transaction:

3 | 8 |
9 | For an even split set each account to: {{ evenSplitValue }} 12 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 38 | 39 | 40 | 41 | 42 |
Name 20 | {{ account.AccountName }} 21 | Amount 27 | 28 | Amount 29 | 36 | 37 |
43 | Values should add up to the total amount 46 |
47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-split-transaction/add-split-transaction.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddSplitTransactionComponent } from './add-split-transaction.component'; 4 | 5 | describe('AddSplitTransactionComponent', () => { 6 | let component: AddSplitTransactionComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AddSplitTransactionComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AddSplitTransactionComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-transaction/add-transaction.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/app/Pages/add/add-transaction/add-transaction.component.css -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-transaction/add-transaction.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Add Transaction:

3 | 4 |
5 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-transaction/add-transaction.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddTransactionComponent } from './add-transaction.component'; 4 | 5 | describe('AddTransactionComponent', () => { 6 | let component: AddTransactionComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AddTransactionComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AddTransactionComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-transaction/add-transaction.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core'; 2 | import { TransactionFormComponent } from 'src/app/Components/transaction-form/transaction-form.component'; 3 | import { DatePipe } from '@angular/common'; 4 | import { TransactionsService } from 'src/app/Services/transactions.service'; 5 | import { Router } from '@angular/router'; 6 | 7 | @Component({ 8 | selector: 'app-add-transaction', 9 | templateUrl: './add-transaction.component.html', 10 | styleUrls: ['./add-transaction.component.css'], 11 | }) 12 | export class AddTransactionComponent implements OnInit, AfterViewInit { 13 | constructor( 14 | private datePipe: DatePipe, 15 | private transactionService: TransactionsService, 16 | private router: Router 17 | ) {} 18 | @ViewChild(TransactionFormComponent, { static: false }) 19 | private transactionForm: TransactionFormComponent; 20 | 21 | ngOnInit(): void {} 22 | 23 | ngAfterViewInit() { 24 | setTimeout(() => { 25 | this.transactionForm.transactionForm.controls.date.setValue( 26 | this.datePipe.transform(new Date(), 'yyyy-MM-dd') 27 | ); 28 | }, 0); 29 | } 30 | 31 | save(form) { 32 | const transaction = { 33 | Date: form.value.date, 34 | AccountID: form.value.account.AccountId, 35 | Category: form.value.category, 36 | Amount: form.value.amount, 37 | Currency: form.value.currency, 38 | Vendor: form.value.vendor, 39 | Merchant: form.value.merchant, 40 | Type: form.value.type, 41 | Note: form.value.note, 42 | Status: form.value.status, 43 | }; 44 | 45 | this.transactionService.addTransaction(transaction).subscribe({ 46 | next: () => this.router.navigate(['/transactions']), 47 | error: () => this.transactionForm.enable(), 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-transfer-transaction/add-transfer-transaction.component.css: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | button { 5 | margin-right: 10px; 6 | } 7 | 8 | @media (min-width: 601px) { 9 | .row { 10 | margin-left: 2px; 11 | } 12 | } 13 | 14 | form { 15 | max-width: 1000px; 16 | } 17 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-transfer-transaction/add-transfer-transaction.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Add Transfer Transaction:

3 | 8 |
9 | 10 | Account From 11 | 16 | 20 | {{ account.AccountName }} 21 | 22 | 23 | 24 |
25 | 26 | 27 | Account To 28 | 33 | 37 | {{ account.AccountName }} 38 | 39 | 40 | 41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-transfer-transaction/add-transfer-transaction.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddTransferTransactionComponent } from './add-transfer-transaction.component'; 4 | 5 | describe('AddTransferTransactionComponent', () => { 6 | let component: AddTransferTransactionComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AddTransferTransactionComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AddTransferTransactionComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/add/add-transfer-transaction/add-transfer-transaction.component.ts: -------------------------------------------------------------------------------- 1 | import { DatePipe } from '@angular/common'; 2 | import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { shareReplay } from 'rxjs/operators'; 5 | import { TransactionFormComponent } from 'src/app/Components/transaction-form/transaction-form.component'; 6 | import { AccountsService } from 'src/app/Services/accounts.service'; 7 | import { TransactionsService } from 'src/app/Services/transactions.service'; 8 | import { accountComparer } from 'src/app/Components/transaction-form/transaction-form.component'; 9 | import { FormControl, FormGroup, Validators } from '@angular/forms'; 10 | import { NotifierService } from 'angular-notifier'; 11 | 12 | @Component({ 13 | templateUrl: './add-transfer-transaction.component.html', 14 | styleUrls: ['./add-transfer-transaction.component.css'], 15 | }) 16 | export class AddTransferTransactionComponent implements OnInit, AfterViewInit { 17 | constructor( 18 | private datePipe: DatePipe, 19 | private transactionsService: TransactionsService, 20 | private accountsService: AccountsService, 21 | private router: Router, 22 | private notifier: NotifierService 23 | ) {} 24 | 25 | @ViewChild(TransactionFormComponent, { static: false }) 26 | private transactionForm: TransactionFormComponent; 27 | 28 | accounts$ = this.accountsService.getAccounts().pipe(shareReplay(1)); 29 | 30 | transferForm = new FormGroup({ 31 | accountFrom: new FormControl(null, [Validators.required]), 32 | accountTo: new FormControl(null, [Validators.required]), 33 | }); 34 | 35 | ngOnInit(): void {} 36 | 37 | ngAfterViewInit() { 38 | setTimeout(() => { 39 | this.transactionForm.transactionForm.controls.date.setValue( 40 | this.datePipe.transform(new Date(), 'yyyy-MM-dd') 41 | ); 42 | this.transactionForm.transactionForm.controls.type.setValue('Transfer'); 43 | this.transactionForm.transactionForm.controls.category.setValue( 44 | 'Transfer' 45 | ); 46 | this.transactionForm.transactionForm.controls.vendor.setValue('Transfer'); 47 | }, 0); 48 | } 49 | 50 | save(form) { 51 | const transaction = { 52 | Date: form.value.date, 53 | AccountID: this.transferForm.value.accountFrom.ID, 54 | Category: form.value.category, 55 | Amount: form.value.amount * -1, 56 | Currency: form.value.currency, 57 | Vendor: form.value.vendor, 58 | Merchant: form.value.merchant, 59 | Type: form.value.type, 60 | Note: form.value.note, 61 | Status: form.value.status, 62 | }; 63 | 64 | this.transactionsService.addTransaction(transaction).subscribe({ 65 | next: () => { 66 | transaction.AccountID = this.transferForm.value.accountTo.ID; 67 | transaction.Amount = form.value.amount; 68 | this.transactionsService 69 | .addTransaction(transaction) 70 | .subscribe({ next: () => this.router.navigate(['/transactions']) }); 71 | }, 72 | error: () => { 73 | this.notifier.notify('error', 'Failed to create transactions'); 74 | this.transactionForm.enable(); 75 | return; 76 | }, 77 | }); 78 | } 79 | 80 | compareAccounts(a1, a2) { 81 | return accountComparer(a1, a2); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/src/app/Pages/dashboard/dashboard.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/app/Pages/dashboard/dashboard.component.css -------------------------------------------------------------------------------- /src/src/app/Pages/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /src/src/app/Pages/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardComponent } from './dashboard.component'; 4 | 5 | describe('DashboardComponent', () => { 6 | let component: DashboardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { TitleService } from 'src/app/Services/title.service'; 3 | 4 | @Component({ 5 | templateUrl: './dashboard.component.html', 6 | styleUrls: ['./dashboard.component.css'], 7 | }) 8 | export class DashboardComponent implements OnInit { 9 | constructor(private titleService: TitleService) { 10 | this.titleService.showBackButton.next(false); 11 | } 12 | 13 | ngOnInit(): void {} 14 | } 15 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeed-coinbase/datafeed-coinbase.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/app/Pages/datafeeds/datafeed-coinbase/datafeed-coinbase.component.css -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeed-coinbase/datafeed-coinbase.component.html: -------------------------------------------------------------------------------- 1 | Coinbase 3 | 4 |
5 |
6 | 9 | 10 |

11 | 12 | 13 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeed-coinbase/datafeed-coinbase.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DatafeedCoinbaseComponent } from './datafeed-coinbase.component'; 4 | 5 | describe('DatafeedCoinbaseComponent', () => { 6 | let component: DatafeedCoinbaseComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DatafeedCoinbaseComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DatafeedCoinbaseComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeed-coinbase/datafeed-coinbase.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FinanceApiRequest } from 'src/app/Services/finance-api.request.service'; 3 | 4 | @Component({ 5 | templateUrl: './datafeed-coinbase.component.html', 6 | styleUrls: ['./datafeed-coinbase.component.css'], 7 | }) 8 | export class DatafeedCoinbaseComponent { 9 | constructor(private financeApi: FinanceApiRequest) {} 10 | 11 | baseUrl = 'https://www.coinbase.com'; 12 | private clientId = this.financeApi.get( 13 | 'DatafeedAuth/GetCoinbaseClientId' 14 | ); 15 | redirectUrl = `${FinanceApiRequest.BASE_URL}DatafeedAuth/CoinBaseAuthentication`; 16 | 17 | LinkCoinBase() { 18 | this.clientId.subscribe({ 19 | next: (id) => { 20 | window.open( 21 | `${ 22 | this.baseUrl 23 | }/oauth/authorize?response_type=code&client_id=${id}&redirect_uri=${ 24 | this.redirectUrl 25 | }&state=${localStorage.getItem( 26 | 'id_token' 27 | )}&account=all&scope=wallet:accounts:read,wallet:transactions:read 28 | `, 29 | '_blank' 30 | ); 31 | }, 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeed-truelayer/datafeed-truelayer.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/app/Pages/datafeeds/datafeed-truelayer/datafeed-truelayer.component.css -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeed-truelayer/datafeed-truelayer.component.html: -------------------------------------------------------------------------------- 1 | TrueLayer 7 |
8 |
9 | 12 | 13 |

14 | 15 | 16 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeed-truelayer/datafeed-truelayer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DatafeedTruelayerComponent } from './datafeed-truelayer.component'; 4 | 5 | describe('DatafeedTruelayerComponent', () => { 6 | let component: DatafeedTruelayerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DatafeedTruelayerComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DatafeedTruelayerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeed-truelayer/datafeed-truelayer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FinanceApiRequest } from 'src/app/Services/finance-api.request.service'; 3 | import { DatafeedsService } from '../../../Services/datafeeds.service'; 4 | 5 | @Component({ 6 | templateUrl: './datafeed-truelayer.component.html', 7 | styleUrls: ['./datafeed-truelayer.component.css'], 8 | }) 9 | export class DatafeedTruelayerComponent implements OnInit { 10 | constructor(private financeApi: FinanceApiRequest) {} 11 | private baseUrl: string; 12 | private clientId = this.financeApi.get( 13 | 'DatafeedAuth/GetTrueLayerClientId' 14 | ); 15 | private redirectUrl: string = `${FinanceApiRequest.BASE_URL}DatafeedAuth/TrueLayerAuthentication`; 16 | 17 | ngOnInit(): void {} 18 | 19 | LinkTrueLayer() { 20 | this.clientId.subscribe({ 21 | next: (id) => { 22 | this.baseUrl = id.startsWith('sandbox-') 23 | ? 'https://auth.truelayer-sandbox.com/' 24 | : 'https://auth.truelayer.com/'; 25 | console.log(this.baseUrl); 26 | window.open( 27 | this.baseUrl + 28 | '?response_type=code&client_id=' + 29 | id + 30 | '&scope=accounts%20balance%20cards%20transactions%20offline_access&redirect_uri=' + 31 | this.redirectUrl + 32 | '&response_mode=form_post&providers=uk-ob-all%20uk-oauth-all%20uk-cs-all%20uk-cs-mock&state=' + 33 | localStorage.getItem('id_token'), 34 | '_blank' 35 | ); 36 | }, 37 | error: () => alert('Something went wrong'), 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeeds-list/datafeeds-list.component.css: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeeds-list/datafeeds-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 41 | 42 | 43 | 44 | 45 |
Provider 5 | {{ datafeed.Provider }} 6 | Vendor 12 | {{ datafeed.VendorName }} 13 | Last Updated 19 | {{ datafeed.LastUpdated | date: "dd-MM-yyyy HH:mm" }} 20 | 26 | 33 | 40 |
46 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeeds-list/datafeeds-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DatafeedsListComponent } from './datafeeds-list.component'; 4 | 5 | describe('DatafeedsListComponent', () => { 6 | let component: DatafeedsListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DatafeedsListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DatafeedsListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeeds-list/datafeeds-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { FinanceApiRequest } from 'src/app/Services/finance-api.request.service'; 3 | import { DatafeedsService } from '../../../Services/datafeeds.service'; 4 | 5 | @Component({ 6 | selector: 'app-datafeeds-list', 7 | templateUrl: './datafeeds-list.component.html', 8 | styleUrls: ['./datafeeds-list.component.css'], 9 | }) 10 | export class DatafeedsListComponent implements OnInit { 11 | constructor( 12 | private datafeedsService: DatafeedsService, 13 | private financeApi: FinanceApiRequest 14 | ) {} 15 | 16 | @Input() provider: string; 17 | datafeeds: any[] = []; 18 | displayedColumns: string[] = ['provider', 'vendor', 'lastUpdated', 'actions']; 19 | 20 | ngOnInit(): void { 21 | this.loadDatafeeds(); 22 | } 23 | 24 | loadDatafeeds() { 25 | this.datafeedsService 26 | .getDatafeeds(this.provider) 27 | .subscribe((d) => (this.datafeeds = d)); 28 | } 29 | 30 | deleteDatafeed(datafeed) { 31 | if ( 32 | confirm( 33 | 'Warning: This will also remove all external account mappings and is not reversable' 34 | ) 35 | ) { 36 | this.datafeedsService 37 | .deleteDatafeed(datafeed.Provider, datafeed.VendorID) 38 | .subscribe(() => this.loadDatafeeds()); 39 | } 40 | } 41 | 42 | reconnectDatafeed(datafeed) { 43 | switch (datafeed.Provider) { 44 | case 'TRUELAYER': 45 | this.reconnectTruelayer(datafeed); 46 | break; 47 | default: 48 | break; 49 | } 50 | } 51 | 52 | reconnectTruelayer(datafeed) { 53 | let baseUrl: string; 54 | let clientId = this.financeApi.get( 55 | 'DatafeedAuth/GetTrueLayerClientId' 56 | ); 57 | let redirectUrl: string = `${FinanceApiRequest.BASE_URL}DatafeedAuth/TrueLayerAuthentication`; 58 | 59 | clientId.subscribe({ 60 | next: (id) => { 61 | baseUrl = id.startsWith('sandbox-') 62 | ? 'https://auth.truelayer-sandbox.com/' 63 | : 'https://auth.truelayer.com/'; 64 | console.log(baseUrl); 65 | window.open( 66 | baseUrl + 67 | '?response_type=code&client_id=' + 68 | id + 69 | '&scope=accounts%20balance%20transactions%20offline_access%20cards&redirect_uri=' + 70 | redirectUrl + 71 | '&response_mode=form_post&providers=uk-ob-all%20uk-oauth-all%20uk-cs-all%20uk-cs-mock&state=' + 72 | localStorage.getItem('id_token') + 73 | '|' + 74 | datafeed._id + 75 | `&provider_id=${datafeed.VendorID}`, 76 | '_blank' 77 | ); 78 | }, 79 | error: () => alert('Something went wrong'), 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeeds-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { DatafeedsComponent } from './datafeeds.component'; 5 | import { DatafeedTruelayerComponent } from './datafeed-truelayer/datafeed-truelayer.component'; 6 | import { DemoGuard } from 'src/app/Guards/demo.guard'; 7 | import { DatafeedCoinbaseComponent } from './datafeed-coinbase/datafeed-coinbase.component'; 8 | 9 | const routes: Routes = [ 10 | { path: '', component: DatafeedsComponent }, 11 | { 12 | path: 'truelayer', 13 | component: DatafeedTruelayerComponent, 14 | canActivate: [DemoGuard], 15 | }, 16 | { 17 | path: 'coinbase', 18 | component: DatafeedCoinbaseComponent, 19 | canActivate: [DemoGuard], 20 | } 21 | ]; 22 | 23 | @NgModule({ 24 | imports: [RouterModule.forChild(routes)], 25 | exports: [RouterModule], 26 | }) 27 | export class DatafeedsRoutingModule {} 28 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeeds.component.css: -------------------------------------------------------------------------------- 1 | mat-card { 2 | max-width: 450px; 3 | } 4 | 5 | .setup-button { 6 | width: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeeds.component.html: -------------------------------------------------------------------------------- 1 |
7 | 8 | Truelayer 10 | 11 | 12 | Integrate with Truelayer to allow you to automatically sync your account 13 | with your bank provider. This integration uses 14 | Open Banking in order to fetch the transactions from your 15 | bank account. To find out more information visit 16 | https://truelayer.com/data-api/ 19 | 20 | 21 |
25 | 34 |
35 |
36 |
37 | 38 | 39 | Coinbase 41 | 42 | 43 | Integrate with Coinbase to allow you to retrieve your investments with the coinbase platform. 44 | 45 | 46 |
50 | 59 |
60 |
61 |
62 |
63 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeeds.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DatafeedsComponent } from './datafeeds.component'; 4 | 5 | describe('DatafeedsComponent', () => { 6 | let component: DatafeedsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DatafeedsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DatafeedsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeeds.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ConfigService } from 'src/app/Services/config.service'; 3 | 4 | @Component({ 5 | selector: 'app-datafeeds', 6 | templateUrl: './datafeeds.component.html', 7 | styleUrls: ['./datafeeds.component.css'], 8 | }) 9 | export class DatafeedsComponent implements OnInit { 10 | constructor(private configService: ConfigService) {} 11 | 12 | isDemo$ = this.configService.getValue('IsDemo'); 13 | 14 | ngOnInit(): void {} 15 | } 16 | -------------------------------------------------------------------------------- /src/src/app/Pages/datafeeds/datafeeds.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { DatafeedsRoutingModule } from './datafeeds-routing.module'; 5 | import { DatafeedsComponent } from './datafeeds.component'; 6 | import { MaterialModule } from 'src/app/Components/material.module'; 7 | import { DatafeedTruelayerComponent } from './datafeed-truelayer/datafeed-truelayer.component'; 8 | import { DatafeedsService } from '../../Services/datafeeds.service'; 9 | import { DatafeedsListComponent } from './datafeeds-list/datafeeds-list.component'; 10 | import { DatafeedCoinbaseComponent } from './datafeed-coinbase/datafeed-coinbase.component'; 11 | import { FlexLayoutModule } from '@angular/flex-layout'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | DatafeedsComponent, 16 | DatafeedTruelayerComponent, 17 | DatafeedsListComponent, 18 | DatafeedCoinbaseComponent, 19 | ], 20 | imports: [CommonModule, DatafeedsRoutingModule, MaterialModule, FlexLayoutModule], 21 | providers: [DatafeedsService], 22 | }) 23 | export class DatafeedsModule {} 24 | -------------------------------------------------------------------------------- /src/src/app/Pages/goals/add-goal/add-goal.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/app/Pages/goals/add-goal/add-goal.component.css -------------------------------------------------------------------------------- /src/src/app/Pages/goals/add-goal/add-goal.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/src/app/Pages/goals/add-goal/add-goal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddGoalComponent } from './add-goal.component'; 4 | 5 | describe('AddGoalComponent', () => { 6 | let component: AddGoalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AddGoalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AddGoalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/goals/add-goal/add-goal.component.ts: -------------------------------------------------------------------------------- 1 | import { DatePipe } from '@angular/common'; 2 | import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { GoalFormComponent } from 'src/app/Components/goal-form/goal-form.component'; 5 | import { GoalsService } from 'src/app/Services/goals.service'; 6 | 7 | @Component({ 8 | templateUrl: './add-goal.component.html', 9 | styleUrls: ['./add-goal.component.css'], 10 | }) 11 | export class AddGoalComponent implements OnInit, AfterViewInit { 12 | constructor( 13 | private datePipe: DatePipe, 14 | private goalService: GoalsService, 15 | private router: Router 16 | ) {} 17 | 18 | @ViewChild(GoalFormComponent, { static: false }) 19 | private goalFormComponent: GoalFormComponent; 20 | 21 | ngOnInit(): void {} 22 | 23 | ngAfterViewInit() { 24 | setTimeout(() => { 25 | this.goalFormComponent.goalForm.controls.date.setValue( 26 | this.datePipe.transform(new Date(), 'yyyy-MM-dd') 27 | ); 28 | }, 0); 29 | } 30 | 31 | save(form) { 32 | const goal = { 33 | Date: form.value.date, 34 | AccountId: form.value.account.AccountId, 35 | GoalAmount: form.value.amount, 36 | Name: form.value.name, 37 | }; 38 | 39 | this.goalService.addGoal(goal).subscribe({ 40 | next: () => this.router.navigate(['/goals']), 41 | error: () => this.goalFormComponent.enable(), 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/src/app/Pages/goals/edit-goal/edit-goal.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/app/Pages/goals/edit-goal/edit-goal.component.css -------------------------------------------------------------------------------- /src/src/app/Pages/goals/edit-goal/edit-goal.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/src/app/Pages/goals/edit-goal/edit-goal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EditGoalComponent } from './edit-goal.component'; 4 | 5 | describe('EditGoalComponent', () => { 6 | let component: EditGoalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ EditGoalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(EditGoalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/goals/edit-goal/edit-goal.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { GoalFormComponent } from 'src/app/Components/goal-form/goal-form.component'; 4 | import { GoalsService } from 'src/app/Services/goals.service'; 5 | 6 | @Component({ 7 | templateUrl: './edit-goal.component.html', 8 | styleUrls: ['./edit-goal.component.css'], 9 | }) 10 | export class EditGoalComponent implements OnInit, AfterViewInit { 11 | constructor( 12 | private goalService: GoalsService, 13 | private route: ActivatedRoute, 14 | private router: Router 15 | ) {} 16 | 17 | @ViewChild(GoalFormComponent, { static: false }) 18 | private goalFormComponent: GoalFormComponent; 19 | 20 | id: string; 21 | goalNotFound = false; 22 | 23 | ngOnInit(): void { 24 | this.id = this.route.snapshot.paramMap.get('id'); 25 | } 26 | 27 | ngAfterViewInit() { 28 | Promise.resolve(() => this.goalFormComponent.disable()).then(() => { 29 | this.goalService.getGoalById(this.id).subscribe({ 30 | next: (transaction) => { 31 | if (transaction) { 32 | this.goalFormComponent.setFormValues(transaction); 33 | this.goalFormComponent.enable(); 34 | } else { 35 | this.goalNotFound = true; 36 | } 37 | }, 38 | error: () => (this.goalNotFound = true), 39 | }); 40 | }); 41 | } 42 | 43 | save(form) { 44 | const goal = { 45 | Id: this.id, 46 | Date: form.value.date, 47 | AccountId: form.value.account.AccountId, 48 | GoalAmount: form.value.amount, 49 | Name: form.value.name, 50 | }; 51 | 52 | this.goalService.updateGoal(goal).subscribe({ 53 | next: () => this.router.navigate(['/goals']), 54 | error: () => this.goalFormComponent.enable(), 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/src/app/Pages/goals/goals.component.css: -------------------------------------------------------------------------------- 1 | .goal-amounts { 2 | font-size: 58px; 3 | font-weight: lighter; 4 | } 5 | 6 | @media (max-width: 385px) { 7 | .goal-amounts { 8 | font-size: 25px; 9 | } 10 | } 11 | 12 | .goalTitle { 13 | font-weight: 300; 14 | } 15 | 16 | /* .goal-card-row { 17 | display: inline-block; 18 | } */ 19 | 20 | .progress-gauge .green-progress ::ng-deep .mat-progress-spinner circle, 21 | .mat-spinner circle { 22 | stroke: #3dc461; 23 | } 24 | 25 | .progress-gauge .yellow-progress ::ng-deep .mat-progress-spinner circle, 26 | .mat-spinner circle { 27 | stroke: #f5f118; 28 | } 29 | 30 | .progress-gauge .red-progress ::ng-deep .mat-progress-spinner circle, 31 | .mat-spinner circle { 32 | stroke: red; 33 | } 34 | 35 | .progress-gauge .spinner-background { 36 | position: absolute; 37 | width: 100px; 38 | height: 100px; 39 | line-height: 80px; 40 | text-align: center; 41 | overflow: hidden; 42 | border-color: rgba(103, 58, 183, 0.12); 43 | border-radius: 50%; 44 | border-style: solid; 45 | border-width: 10px; 46 | font-size: 25px; 47 | } 48 | 49 | .goal-card { 50 | margin-bottom: 20px; 51 | } 52 | -------------------------------------------------------------------------------- /src/src/app/Pages/goals/goals.component.html: -------------------------------------------------------------------------------- 1 |
2 | Add 3 |
4 | 5 |
6 | 7 | {{ goal.AccountName }} - {{ goal.Name }} 10 | 11 |
12 | {{ goal.CurrentAmount | currency }} / 14 | {{ goal.GoalAmount | currency }} 16 |
17 |
18 | {{ goalProgress.value | number: "1.0-0" }}% 19 |
20 |
28 | 34 | 35 |
36 |
37 |
38 | By: {{ goal.Date | date: "dd/MM/yyyy" }} 39 |
40 | 41 | 42 | Edit 45 | 48 | 49 |
50 |
51 | -------------------------------------------------------------------------------- /src/src/app/Pages/goals/goals.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { GoalsComponent } from './goals.component'; 4 | 5 | describe('GoalsComponent', () => { 6 | let component: GoalsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ GoalsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(GoalsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/goals/goals.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { IsLoadingService } from '@service-work/is-loading'; 4 | import { NotifierService } from 'angular-notifier'; 5 | import { tap } from 'rxjs/operators'; 6 | import { GoalsService } from 'src/app/Services/goals.service'; 7 | 8 | @Component({ 9 | templateUrl: './goals.component.html', 10 | styleUrls: ['./goals.component.css'], 11 | }) 12 | export class GoalsComponent implements OnInit { 13 | constructor( 14 | private goalsService: GoalsService, 15 | private router: Router, 16 | private notifier: NotifierService, 17 | private loadingService: IsLoadingService 18 | ) {} 19 | 20 | goals$ = this.goalsService 21 | .getGoals() 22 | .pipe(tap(() => this.loadingService.remove({ key: ['default', 'goals'] }))); 23 | 24 | ngOnInit(): void { 25 | this.loadingService.add({ key: ['default', 'goals'] }); 26 | } 27 | 28 | deleteGoal(goalId: string) { 29 | if (confirm('Are you sure you want to delete this goal')) { 30 | this.goalsService.deleteGoal(goalId).subscribe(() => { 31 | this.notifier.notify('success', 'Goal Delete Successfully'); 32 | this.goals$ = this.goalsService.getGoals(); 33 | }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/src/app/Pages/login/login.component.css: -------------------------------------------------------------------------------- 1 | .title { 2 | margin-top: 10px; 3 | color: black; 4 | } 5 | 6 | .login-card { 7 | max-width: 600px; 8 | min-width: 350px; 9 | 10 | text-align: center; 11 | margin: auto; 12 | justify-content: center; 13 | align-items: center; 14 | box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.2); 15 | border-radius: 10px; 16 | z-index: 2; 17 | padding: 0; 18 | opacity: 0.98; 19 | } 20 | 21 | mat-progress-bar { 22 | z-index: 5; 23 | position: absolute; 24 | } 25 | 26 | mat-card-content { 27 | padding: 25px; 28 | } 29 | 30 | .login-container { 31 | height: 100%; 32 | width: 100%; 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | } 37 | 38 | .login-background { 39 | height: 100%; 40 | width: 100%; 41 | position: absolute; 42 | background: url("../../../assets/city.jpg") no-repeat; 43 | z-index: 1; 44 | -webkit-background-size: cover; 45 | -moz-background-size: cover; 46 | -o-background-size: cover; 47 | background-size: cover; 48 | } 49 | 50 | mat-form-field, 51 | button { 52 | width: 100%; 53 | } 54 | 55 | #login-submit { 56 | margin-top: 10px; 57 | } 58 | 59 | #login-submit:disabled { 60 | cursor: not-allowed; 61 | } 62 | -------------------------------------------------------------------------------- /src/src/app/Pages/login/login.component.html: -------------------------------------------------------------------------------- 1 | 75 | -------------------------------------------------------------------------------- /src/src/app/Pages/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormGroup, FormControl, Validators } from '@angular/forms'; 3 | import { AuthService } from 'src/app/Services/auth.service'; 4 | import { FinanceApiRequest } from 'src/app/Services/finance-api.request.service'; 5 | import { ActivatedRoute, Router } from '@angular/router'; 6 | import { ConfigService } from 'src/app/Services/config.service'; 7 | 8 | @Component({ 9 | templateUrl: './login.component.html', 10 | styleUrls: ['./login.component.css'], 11 | }) 12 | export class LoginComponent implements OnInit { 13 | constructor( 14 | private authService: AuthService, 15 | private router: Router, 16 | private config: ConfigService, 17 | private route: ActivatedRoute 18 | ) {} 19 | 20 | loadingBar: boolean = false; 21 | error: string; 22 | isDemo = this.config.getValue('IsDemo'); 23 | 24 | loginForm = new FormGroup({ 25 | username: new FormControl(null, [Validators.required]), 26 | password: new FormControl(null, [Validators.required]), 27 | }); 28 | 29 | ngOnInit(): void {} 30 | 31 | login(): void { 32 | this.error = null; 33 | this.loginForm.disable(); 34 | this.loadingBar = true; 35 | 36 | const username = this.loginForm.value.username; 37 | const password = this.loginForm.value.password; 38 | 39 | this.authService.login(username, password).subscribe({ 40 | next: (token) => { 41 | if (token) { 42 | FinanceApiRequest.setToken(token); 43 | if (this.route.snapshot.queryParams['redirect']) { 44 | this.router.navigate([this.route.snapshot.queryParams['redirect']]); 45 | } else { 46 | this.router.navigate(['']); 47 | } 48 | } 49 | this.error = 'Username or Password Invalid'; 50 | }, 51 | complete: () => { 52 | this.loginForm.enable(); 53 | this.loadingBar = false; 54 | }, 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/src/app/Pages/register/register.component.css: -------------------------------------------------------------------------------- 1 | .title { 2 | margin-top: 10px; 3 | color: black; 4 | } 5 | 6 | .register-card { 7 | max-width: 600px; 8 | min-width: 350px; 9 | 10 | text-align: center; 11 | margin: auto; 12 | justify-content: center; 13 | align-items: center; 14 | box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.2); 15 | border-radius: 10px; 16 | z-index: 2; 17 | padding: 0; 18 | opacity: 0.98; 19 | } 20 | 21 | mat-progress-bar { 22 | z-index: 5; 23 | position: absolute; 24 | } 25 | 26 | mat-card-content { 27 | padding: 25px; 28 | } 29 | 30 | .register-container { 31 | height: 100%; 32 | width: 100%; 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | } 37 | 38 | .register-background { 39 | height: 100%; 40 | width: 100%; 41 | position: absolute; 42 | background: url("../../../assets/city.jpg") no-repeat; 43 | z-index: 1; 44 | -webkit-background-size: cover; 45 | -moz-background-size: cover; 46 | -o-background-size: cover; 47 | background-size: cover; 48 | } 49 | 50 | mat-form-field, 51 | button { 52 | width: 100%; 53 | } 54 | 55 | #register-submit { 56 | margin-top: 10px; 57 | } 58 | 59 | #register-submit:disabled { 60 | cursor: not-allowed; 61 | } 62 | -------------------------------------------------------------------------------- /src/src/app/Pages/register/register.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 9 | 10 | 11 |
12 |

Finance Manager

13 | Sign up Below 14 |

15 | 16 | 17 | First Name 18 | 25 | First Name Required 26 | 27 |
28 | 29 | Last Name 30 | 37 | Last Name Required 38 | 39 |
40 | 41 | Username 42 | 49 | Username Required 50 | 51 |
52 | 53 | Password 54 | 61 | Password Required 62 | 63 |
64 | 65 | Confirm Password 66 | 73 | Password Required 74 | 75 |
76 | {{ error }} 77 | 86 | 89 |
90 |
91 |
92 |
93 | -------------------------------------------------------------------------------- /src/src/app/Pages/register/register.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RegisterComponent } from './register.component'; 4 | 5 | describe('RegisterComponent', () => { 6 | let component: RegisterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ RegisterComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RegisterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormGroup, FormControl, Validators } from '@angular/forms'; 3 | import { AuthService } from 'src/app/Services/auth.service'; 4 | import { FinanceApiRequest } from 'src/app/Services/finance-api.request.service'; 5 | import { Router } from '@angular/router'; 6 | import { NotifierService } from 'angular-notifier'; 7 | 8 | @Component({ 9 | templateUrl: './register.component.html', 10 | styleUrls: ['./register.component.css'], 11 | }) 12 | export class RegisterComponent implements OnInit { 13 | constructor( 14 | private authService: AuthService, 15 | private router: Router, 16 | private notifier: NotifierService 17 | ) {} 18 | 19 | loadingBar: boolean = false; 20 | error: string; 21 | 22 | registerForm = new FormGroup({ 23 | firstName: new FormControl(null, [Validators.required]), 24 | lastName: new FormControl(null, [Validators.required]), 25 | username: new FormControl(null, [Validators.required]), 26 | password: new FormControl(null, [Validators.required]), 27 | confirmPassword: new FormControl(null, [Validators.required]), 28 | }); 29 | 30 | ngOnInit(): void {} 31 | 32 | register() { 33 | this.registerForm.disable(); 34 | this.loadingBar = true; 35 | if ( 36 | this.registerForm.value.password !== 37 | this.registerForm.value.confirmPassword 38 | ) { 39 | this.error = 'Passwords do not Match'; 40 | } 41 | 42 | const body = { 43 | FirstName: this.registerForm.value.firstName, 44 | LastName: this.registerForm.value.lastName, 45 | Username: this.registerForm.value.username, 46 | Password: this.registerForm.value.password, 47 | }; 48 | 49 | this.authService.register(body).subscribe({ 50 | next: (clientId) => { 51 | if (clientId) { 52 | this.notifier.notify('success', 'Registered successfully'); 53 | this.router.navigate(['login']); 54 | } 55 | this.error = 'Failed to Register'; 56 | this.registerForm.enable(); 57 | this.loadingBar = false; 58 | }, 59 | error: (ex) => this.processRegisterError(ex), 60 | complete: () => { 61 | this.registerForm.enable(); 62 | this.loadingBar = false; 63 | }, 64 | }); 65 | } 66 | 67 | processRegisterError(ex) { 68 | console.log(ex); 69 | this.notifier.notify('error', ex.error.error || 'Something went Wrong'); 70 | this.registerForm.enable(); 71 | this.loadingBar = false; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/src/app/Pages/settings/settings.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/app/Pages/settings/settings.component.css -------------------------------------------------------------------------------- /src/src/app/Pages/settings/settings.component.html: -------------------------------------------------------------------------------- 1 |

Interface:

2 | 3 | 4 | Theme 5 | 6 | Light 7 | Dark 8 | 9 | 10 | 11 |
12 | 13 | 14 | Table Theme 15 | 16 | Alpine (Default) 17 | Balham 18 | Material 19 | 20 | 21 | 22 |
23 | 24 | 25 | Show Wealth Pages 26 | 27 | 28 | 29 |
30 | 31 | -------------------------------------------------------------------------------- /src/src/app/Pages/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsComponent } from './settings.component'; 4 | 5 | describe('SettingsComponent', () => { 6 | let component: SettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SettingsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SettingsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ConfigService } from 'src/app/Services/config.service'; 3 | import { ThemeService } from 'src/app/Services/theme.service'; 4 | 5 | @Component({ 6 | templateUrl: './settings.component.html', 7 | styleUrls: ['./settings.component.css'], 8 | }) 9 | export class SettingsComponent { 10 | constructor(private configService: ConfigService) {} 11 | 12 | theme: string = ThemeService.CurrentTheme.value; 13 | tableTheme: string = localStorage.getItem('table-theme') ?? 'ag-theme-alpine' 14 | enable_wealth: string = localStorage.getItem('enable_wealth') ?? "true"; 15 | 16 | save() { 17 | ThemeService.ChangeTheme(this.theme); 18 | this.configService.setClientValue('enable_wealth', this.enable_wealth); 19 | localStorage.setItem('table-theme', this.tableTheme); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/src/app/Pages/transaction-details/transaction-details.component.css: -------------------------------------------------------------------------------- 1 | .transaction-logo { 2 | height: 50px; 3 | width: 50px; 4 | margin-right: 20px; 5 | } 6 | -------------------------------------------------------------------------------- /src/src/app/Pages/transaction-details/transaction-details.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 | 16 | 17 |
18 | 19 | 23 | 24 |

25 | Could not find Transaction 26 |

27 |
28 | -------------------------------------------------------------------------------- /src/src/app/Pages/transaction-details/transaction-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TransactionDetailsComponent } from './transaction-details.component'; 4 | 5 | describe('TransactionDetailsComponent', () => { 6 | let component: TransactionDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TransactionDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TransactionDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pages/transactions/transactions.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/app/Pages/transactions/transactions.component.css -------------------------------------------------------------------------------- /src/src/app/Pages/transactions/transactions.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 20 | -------------------------------------------------------------------------------- /src/src/app/Pages/transactions/transactions.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TransactionsComponent } from './transactions.component'; 4 | 5 | describe('TransactionsComponent', () => { 6 | let component: TransactionsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TransactionsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TransactionsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/src/app/Pipes/pipes.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | 5 | 6 | @NgModule({ 7 | declarations: [], 8 | imports: [ 9 | CommonModule 10 | ] 11 | }) 12 | export class PipesModule { } 13 | -------------------------------------------------------------------------------- /src/src/app/Services/accounts.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AccountsService } from './accounts.service'; 4 | 5 | describe('AccountsService', () => { 6 | let service: AccountsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(AccountsService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/src/app/Services/accounts.service.ts: -------------------------------------------------------------------------------- 1 | import { Account } from './../Models/account.model'; 2 | import { Injectable } from '@angular/core'; 3 | import { FinanceApiRequest } from './finance-api.request.service'; 4 | import { Observable } from 'rxjs'; 5 | import { AccountSettings } from '../Models/account-settings'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class AccountsService { 11 | constructor(private financeApi: FinanceApiRequest) {} 12 | 13 | getAccounts(): Observable { 14 | return this.financeApi.get('account'); 15 | } 16 | 17 | getAccountById(id: string): Observable { 18 | return this.financeApi.get(`account/${id}`); 19 | } 20 | 21 | addNewAccount(account: any) { 22 | return this.financeApi.post(`account`, JSON.stringify(account)); 23 | } 24 | 25 | deleteAccount(accountId: string) { 26 | return this.financeApi.delete(`account/${accountId}`); 27 | } 28 | 29 | getSpentThisWeek(accountId: string) { 30 | return this.financeApi.get(`account/${accountId}/GetSpentThisWeek`); 31 | } 32 | 33 | getAccountSettings(accountId: string): Observable { 34 | return this.financeApi.get(`account/${accountId}/GetAccountSettings`); 35 | } 36 | 37 | setAccountSettings(accountSettings: AccountSettings) { 38 | return this.financeApi.post( 39 | `account/${accountSettings.AccountID}/SetAccountSettings`, 40 | JSON.stringify(accountSettings) 41 | ); 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/src/app/Services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | let service: AuthService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(AuthService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/src/app/Services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FinanceApiRequest } from './finance-api.request.service'; 3 | import { Observable, of } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | import { HttpClient } from '@angular/common/http'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class AuthService { 11 | constructor(private http: HttpClient) {} 12 | 13 | login(username: string, password: string): Observable { 14 | const body = { 15 | username, 16 | password, 17 | }; 18 | 19 | return this.http 20 | .post(`${FinanceApiRequest.BASE_URL}auth/authenticate`, body) 21 | .pipe( 22 | catchError((e) => { 23 | console.log(e); 24 | throw e; 25 | }) 26 | ) 27 | .pipe(catchError(() => of(null))); 28 | } 29 | 30 | register(formValues: any): Observable { 31 | return this.http.post( 32 | `${FinanceApiRequest.BASE_URL}client`, 33 | formValues 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/src/app/Services/config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ConfigService } from './config.service'; 4 | 5 | describe('ConfigService', () => { 6 | let service: ConfigService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ConfigService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/src/app/Services/config.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { BehaviorSubject, Observable } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class ConfigService { 9 | constructor(private http: HttpClient) {} 10 | 11 | observers: any = {}; 12 | 13 | private config; 14 | 15 | async getValue(key: string): Promise { 16 | if (!this.config) { 17 | this.config = await this.http 18 | .get('assets/config.json') 19 | .toPromise() 20 | .catch(() => null); 21 | } 22 | 23 | return this.config[key]; 24 | } 25 | 26 | getClientValue(key: string): BehaviorSubject { 27 | const obs = new BehaviorSubject(localStorage.getItem(key)) 28 | 29 | if(!this.observers[key]){ 30 | this.observers[key] = []; 31 | } 32 | 33 | this.observers[key].push(obs); 34 | return obs; 35 | } 36 | 37 | setClientValue(key: string, value: string) { 38 | localStorage.setItem(key, value); 39 | 40 | if(this.observers[key] && this.observers[key].length > 0){ 41 | this.observers[key].forEach(o => o.next(value)); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/src/app/Services/datafeeds.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { DatafeedsService } from './datafeeds.service'; 4 | 5 | describe('DatafeedsService', () => { 6 | let service: DatafeedsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(DatafeedsService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/src/app/Services/datafeeds.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FinanceApiRequest } from 'src/app/Services/finance-api.request.service'; 3 | import { Params } from '@angular/router'; 4 | import { Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class DatafeedsService { 11 | constructor(private financeApi: FinanceApiRequest) {} 12 | 13 | getDatafeeds(datafeedType?: string): Observable { 14 | var query: Params = {}; 15 | if (datafeedType) { 16 | query.datafeedType = datafeedType.toUpperCase(); 17 | } 18 | 19 | return this.financeApi.get('datafeed', query); 20 | } 21 | 22 | deleteDatafeed(provider: string, vendorId: string): Observable { 23 | var query: Params = { 24 | provider, 25 | vendorId, 26 | }; 27 | return this.financeApi.delete('datafeed/DeleteDatafeed', query); 28 | } 29 | 30 | getExternalAccounts(): Observable { 31 | return this.financeApi.get('datafeed/GetExternalAccounts'); 32 | } 33 | 34 | doesAccountHaveExternalMappings(accountId: string): Observable { 35 | return this.financeApi 36 | .get('datafeed/GetMappedExternalAccounts', { accountId }) 37 | .pipe( 38 | map((accounts) => { 39 | return accounts.some((a) => a.MappedAccount === accountId); 40 | }) 41 | ); 42 | } 43 | 44 | addExternalAccountMapping( 45 | provider: string, 46 | vendorID: string, 47 | accountID: string, 48 | externalAccountID: string, 49 | extraDetails: any 50 | ): Observable { 51 | var body = { 52 | provider, 53 | vendorID, 54 | accountID, 55 | externalAccountID, 56 | extraDetails 57 | }; 58 | 59 | return this.financeApi.post( 60 | 'datafeed/AddExternalAccountMapping', 61 | JSON.stringify(body) 62 | ); 63 | } 64 | 65 | removeExternalAccountMapping(accountId: string, externalAccountId: string) { 66 | var query = { 67 | accountId, 68 | externalAccountId, 69 | }; 70 | 71 | return this.financeApi.delete( 72 | 'datafeed/RemoveExternalAccountMapping', 73 | query 74 | ); 75 | } 76 | 77 | refreshAccount(accountId: string) { 78 | var query = { 79 | accountId, 80 | }; 81 | 82 | return this.financeApi.get('datafeed/RefreshAccount', query); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/src/app/Services/finance-api.request.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { FinanceApi.RequestService } from './finance-api.request.service'; 4 | 5 | describe('FinanceApi.RequestService', () => { 6 | let service: FinanceApi.RequestService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(FinanceApi.RequestService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/src/app/Services/finance-api.request.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | import { Observable, of } from 'rxjs'; 4 | import { Params } from '@angular/router'; 5 | import { catchError } from 'rxjs/operators'; 6 | import { NotifierService } from 'angular-notifier'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class FinanceApiRequest { 12 | constructor(private http: HttpClient) {} 13 | public static BASE_URL = 'http://localhost:5001/api/'; 14 | public static get Token(): string | null { 15 | return localStorage.getItem('id_token'); 16 | } 17 | 18 | public static setToken(token: string): void { 19 | if (token) { 20 | return localStorage.setItem('id_token', token); 21 | } 22 | return localStorage.removeItem('id_token'); 23 | } 24 | 25 | public static LoadBaseUrl(http: HttpClient): () => Promise { 26 | return async () => { 27 | const config = await http 28 | .get('assets/config.json') 29 | .toPromise() 30 | .catch(() => null); 31 | if (config && config.FinanceApiUrl && config.FinanceApiUrl !== '') { 32 | FinanceApiRequest.BASE_URL = config.FinanceApiUrl; 33 | } 34 | 35 | if (!FinanceApiRequest.BASE_URL.endsWith('/')) { 36 | FinanceApiRequest.BASE_URL = FinanceApiRequest.BASE_URL + '/'; 37 | } 38 | }; 39 | } 40 | 41 | public get(url: string, queryParams?: Params): Observable { 42 | return this.http 43 | .get(`${FinanceApiRequest.BASE_URL}${url}`, { 44 | params: queryParams, 45 | headers: this.authHeader, 46 | }) 47 | .pipe( 48 | catchError((ex) => { 49 | console.log(ex); 50 | throw ex; 51 | }) 52 | ); 53 | } 54 | 55 | public post(url: string, body: any, queryParams?: Params): Observable { 56 | return this.http 57 | .post(`${FinanceApiRequest.BASE_URL}${url}`, body, { 58 | params: queryParams, 59 | headers: this.authHeader, 60 | }) 61 | .pipe( 62 | catchError((ex) => { 63 | console.log(ex); 64 | throw ex; 65 | }) 66 | ); 67 | } 68 | 69 | public put(url: string, body: any, queryParams?: Params): Observable { 70 | return this.http 71 | .put(`${FinanceApiRequest.BASE_URL}${url}`, body, { 72 | params: queryParams, 73 | headers: this.authHeader, 74 | }) 75 | .pipe( 76 | catchError((ex) => { 77 | console.log(ex); 78 | throw ex; 79 | }) 80 | ); 81 | } 82 | 83 | public delete(url: string, queryParams?: Params): Observable { 84 | return this.http 85 | .delete(`${FinanceApiRequest.BASE_URL}${url}`, { 86 | params: queryParams, 87 | headers: this.authHeader, 88 | }) 89 | .pipe( 90 | catchError((ex) => { 91 | console.log(ex); 92 | throw ex; 93 | }) 94 | ); 95 | } 96 | 97 | get authHeader(): HttpHeaders { 98 | return new HttpHeaders() 99 | .set('Authorization', `Bearer ${FinanceApiRequest.Token}`) 100 | .append('Content-Type', 'application/json'); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/src/app/Services/goals.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { GoalsService } from './goals.service'; 4 | 5 | describe('GoalsService', () => { 6 | let service: GoalsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(GoalsService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/src/app/Services/goals.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { FinanceApiRequest } from './finance-api.request.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class GoalsService { 9 | constructor(private financeApi: FinanceApiRequest) {} 10 | 11 | addGoal(goal: any): Observable { 12 | return this.financeApi.post('goal', JSON.stringify(goal)); 13 | } 14 | 15 | getGoals(): Observable { 16 | return this.financeApi.get('goal'); 17 | } 18 | 19 | getGoalById(goalId: string) { 20 | return this.financeApi.get(`goal/${goalId}`); 21 | } 22 | 23 | deleteGoal(goalId: string) { 24 | return this.financeApi.delete(`goal/${goalId}`); 25 | } 26 | 27 | updateGoal(goal: any) { 28 | return this.financeApi.put('goal', JSON.stringify(goal)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/src/app/Services/notification.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { NotificationService } from './notification.service'; 4 | 5 | describe('NotificationService', () => { 6 | let service: NotificationService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(NotificationService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/src/app/Services/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { catchError, map, shareReplay, tap } from 'rxjs/operators'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable, of, Subject } from 'rxjs'; 4 | import { FinanceApiRequest } from './finance-api.request.service'; 5 | import { DatePipe } from '@angular/common'; 6 | import { NotifierService } from 'angular-notifier'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class NotificationService { 12 | lastChecked: Date; 13 | newNotificationReceived: Subject = new Subject(); 14 | private _triggerUnreadCountRefresh: Subject = new Subject(); 15 | 16 | constructor( 17 | private financeApi: FinanceApiRequest, 18 | private datePipe: DatePipe, 19 | private notifier: NotifierService 20 | ) {} 21 | 22 | getNotifications(): Observable { 23 | return this.financeApi.get('notification'); 24 | } 25 | 26 | updateNotificationReadStatus( 27 | notificationId: string, 28 | isRead: boolean 29 | ): Observable { 30 | return this.financeApi 31 | .put(`notification/${notificationId}/UpdateReadStatus`, null, { 32 | isRead, 33 | }) 34 | .pipe(catchError(() => of(false))) 35 | .pipe(map(() => true)); 36 | } 37 | 38 | deleteNotification(notificationId: string): Observable { 39 | return this.financeApi 40 | .delete(`notification/${notificationId}`) 41 | .pipe(catchError(() => of(false))) 42 | .pipe(map(() => true)); 43 | } 44 | 45 | getUnreadNotificationCount(): Observable { 46 | return this.financeApi 47 | .get( 48 | 'notification/GetUnreadNotificationCount', 49 | { 50 | lastChecked: this.datePipe.transform( 51 | (this.lastChecked ?? new Date()).toUTCString(), 52 | 'yyyy-MM-ddTHH:mm:ss' 53 | ), 54 | } 55 | ) 56 | .pipe(tap(() => (this.lastChecked = new Date()))) 57 | .pipe( 58 | tap((response) => { 59 | if ( 60 | response?.NewNotifications && 61 | response.NewNotifications.length > 0 62 | ) { 63 | this.newNotificationReceived.next(response.NewNotifications); 64 | response.NewNotifications.forEach((not) => 65 | this.notifier.notify( 66 | not.NotificationType.toLowerCase(), 67 | not.Message, 68 | not.ID 69 | ) 70 | ); 71 | } 72 | }) 73 | ); 74 | } 75 | 76 | getUnreadPoll(): Observable { 77 | return new Observable((obs) => { 78 | // Get first value 79 | this.getUnreadNotificationCount().subscribe({ 80 | next: (result) => obs.next(result.Count), 81 | }); 82 | 83 | // Get new value every 15 seconds 84 | setInterval(() => { 85 | this.getUnreadNotificationCount().subscribe({ 86 | next: (result) => { 87 | obs.next(result.Count); 88 | }, 89 | }); 90 | }, 15000); 91 | 92 | // Allow for manual refresh 93 | this._triggerUnreadCountRefresh.subscribe(() => { 94 | this.getUnreadNotificationCount().subscribe({ 95 | next: (result) => obs.next(result.Count), 96 | }); 97 | }); 98 | }).pipe(shareReplay(1)); 99 | } 100 | 101 | triggerUnreadRefresh(): void { 102 | this._triggerUnreadCountRefresh.next(); 103 | } 104 | } 105 | 106 | export interface Notification { 107 | ID: string; 108 | Message: string; 109 | NotificationType: 'Info' | 'Warning' | 'Error'; 110 | Details: any; 111 | MarkedAsRead: boolean; 112 | DateCreated: Date; 113 | } 114 | 115 | export interface NotificationCountResponse { 116 | Count: number; 117 | NewNotifications?: Notification[]; 118 | } 119 | -------------------------------------------------------------------------------- /src/src/app/Services/service.module.ts: -------------------------------------------------------------------------------- 1 | import { APP_INITIALIZER, NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { AuthService } from './auth.service'; 4 | import { FinanceApiRequest } from './finance-api.request.service'; 5 | import { TitleService } from './title.service'; 6 | import { ConfigService } from './config.service'; 7 | import { StatisticsService } from './statistics.service'; 8 | import { GoalsService } from './goals.service'; 9 | import { ThemeService } from './theme.service'; 10 | import { NotificationService } from './notification.service'; 11 | 12 | @NgModule({ 13 | declarations: [], 14 | imports: [CommonModule], 15 | providers: [ 16 | FinanceApiRequest, 17 | AuthService, 18 | TitleService, 19 | ConfigService, 20 | StatisticsService, 21 | GoalsService, 22 | NotificationService, 23 | ThemeService, 24 | { 25 | provide: APP_INITIALIZER, 26 | useFactory: ThemeService.LoadTheme$, 27 | deps: [], 28 | multi: true, 29 | }, 30 | ], 31 | }) 32 | export class ServiceModule {} 33 | -------------------------------------------------------------------------------- /src/src/app/Services/statistics.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { StatisticsService } from './statistics.service'; 4 | 5 | describe('StatisticsService', () => { 6 | let service: StatisticsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(StatisticsService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/src/app/Services/statistics.service.ts: -------------------------------------------------------------------------------- 1 | import { DatePipe } from '@angular/common'; 2 | import { Injectable } from '@angular/core'; 3 | import { FinanceApiRequest } from './finance-api.request.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class StatisticsService { 9 | constructor( 10 | private financeApi: FinanceApiRequest, 11 | private datePipe: DatePipe 12 | ) {} 13 | 14 | getBalanceHistory(dateFrom?: Date) { 15 | let date = this.datePipe.transform(dateFrom, 'yyyy-MM-ddTHH:mm:ss'); 16 | return this.financeApi.get( 17 | 'statistics/GetBalanceHistory', 18 | dateFrom 19 | ? { 20 | dateFrom: date, 21 | } 22 | : null 23 | ); 24 | } 25 | 26 | getSpentAmountPerCategory() { 27 | return this.financeApi.get('statistics/GetSpentAmountPerCategory'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/src/app/Services/theme.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as Chart from 'chart.js'; 3 | import { BehaviorSubject } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class ThemeService { 9 | constructor() {} 10 | 11 | public static LoadTheme$(): () => Promise { 12 | return () => 13 | new Promise((resolve) => { 14 | ThemeService.LoadTheme(); 15 | resolve(); 16 | }); 17 | } 18 | 19 | public static CurrentTheme = new BehaviorSubject('light'); 20 | 21 | public static LoadTheme() { 22 | var element = document.getElementById('app-entry'); 23 | this.CurrentTheme.next(localStorage.getItem('theme') || 'light'); 24 | if (this.CurrentTheme.value == 'light') { 25 | element.classList.remove('finance-dark-theme'); 26 | Chart.defaults.global.defaultFontColor = '#646464'; 27 | } else { 28 | element.classList.add('finance-dark-theme'); 29 | Chart.defaults.global.defaultFontColor = '#f2f2f2'; 30 | } 31 | } 32 | 33 | public static ChangeTheme(theme: string) { 34 | localStorage.setItem('theme', theme); 35 | this.LoadTheme(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/src/app/Services/title.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TitleService } from './title.service'; 4 | 5 | describe('TitleService', () => { 6 | let service: TitleService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(TitleService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/src/app/Services/title.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { ConfigService } from './config.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class TitleService { 9 | constructor(private configService: ConfigService) { 10 | this.configService.getValue('SiteName').then((name) => { 11 | this.baseSiteName = name || 'Finance Manager'; 12 | this.setDocumentTitle(); 13 | }); 14 | } 15 | 16 | baseSiteName = ''; 17 | public title: BehaviorSubject = new BehaviorSubject(null); 18 | public showBackButton: BehaviorSubject = new BehaviorSubject< 19 | boolean 20 | >(true); 21 | 22 | public setTitle(title: string) { 23 | this.title.next(title); 24 | this.setDocumentTitle(); 25 | } 26 | 27 | public getTitle() { 28 | return this.title.value; 29 | } 30 | 31 | public resetTitle() { 32 | this.title.next(null); 33 | } 34 | 35 | private setDocumentTitle() { 36 | if (this.getTitle() && this.getTitle().length > 0) { 37 | document.title = `${this.getTitle()} - ${this.baseSiteName}`; 38 | } else { 39 | document.title = this.baseSiteName; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/src/app/Services/transactions.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TransactionsService } from './transactions.service'; 4 | 5 | describe('TransactionsService', () => { 6 | let service: TransactionsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(TransactionsService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/src/app/Services/transactions.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FinanceApiRequest } from './finance-api.request.service'; 3 | import { Observable } from 'rxjs'; 4 | import { Params } from '@angular/router'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class TransactionsService { 10 | constructor(private financeApi: FinanceApiRequest) {} 11 | 12 | getTransactions(filters?: { accountId?: string }): Observable { 13 | return this.financeApi.get('transaction', filters); 14 | } 15 | 16 | getTransactionById(transactionId: string): Observable { 17 | return this.financeApi.get(`transaction/${transactionId}`); 18 | } 19 | 20 | addTransaction(transaction: any): Observable { 21 | return this.financeApi.post('transaction', JSON.stringify(transaction)); 22 | } 23 | 24 | updateTransaction(transaction: any): Observable { 25 | return this.financeApi.put(`transaction/${transaction.ID}`, JSON.stringify(transaction)); 26 | } 27 | 28 | deleteTransaction(transactionId: string): Observable { 29 | return this.financeApi.delete(`transaction/${transactionId}`); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/src/app/Services/wealth/assets.service.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from '../../Models/asset.model'; 2 | import { Injectable } from '@angular/core'; 3 | import { FinanceApiRequest } from '../finance-api.request.service'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class AssetsService { 10 | constructor(private financeApi: FinanceApiRequest) {} 11 | 12 | getAssets(): Observable { 13 | return this.financeApi.get('wealth/assets'); 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/src/app/Services/wealth/trades.service.ts: -------------------------------------------------------------------------------- 1 | import { Trade } from '../../Models/trade.model'; 2 | import { Injectable } from '@angular/core'; 3 | import { FinanceApiRequest } from '../finance-api.request.service'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class TradesService { 10 | constructor(private financeApi: FinanceApiRequest) {} 11 | 12 | getTrades(): Observable { 13 | return this.financeApi.get('wealth/trades'); 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/app/app.component.css -------------------------------------------------------------------------------- /src/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'finance-manager'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('finance-manager'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('finance-manager app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { SwUpdate } from '@angular/service-worker'; 3 | import { 4 | Router, 5 | RouteConfigLoadStart, 6 | RouteConfigLoadEnd, 7 | NavigationStart, 8 | ActivatedRoute, 9 | NavigationEnd, 10 | ActivatedRouteSnapshot, 11 | ActivationEnd, 12 | } from '@angular/router'; 13 | import { IsLoadingService } from '@service-work/is-loading'; 14 | import { TitleService } from './Services/title.service'; 15 | import { MatSnackBar } from '@angular/material/snack-bar'; 16 | import { filter, map, switchMap } from 'rxjs/operators'; 17 | 18 | @Component({ 19 | selector: 'app-root', 20 | templateUrl: './app.component.html', 21 | styleUrls: ['./app.component.css'], 22 | }) 23 | export class AppComponent { 24 | title = 'finance-manager'; 25 | 26 | constructor( 27 | private router: Router, 28 | private updates: SwUpdate, 29 | private loadingService: IsLoadingService, 30 | private titleSerivce: TitleService, 31 | private snackBar: MatSnackBar, 32 | private activatedRoute: ActivatedRoute 33 | ) { 34 | this.router.events.subscribe((event) => { 35 | if (event instanceof RouteConfigLoadStart) { 36 | this.loadingService.add({ key: ['default', 'lazy-loading'] }); 37 | } 38 | if (event instanceof RouteConfigLoadEnd) { 39 | this.loadingService.remove({ key: ['default', 'lazy-loading'] }); 40 | } 41 | if (event instanceof NavigationStart) { 42 | this.loadingService.remove(); 43 | this.titleSerivce.resetTitle(); 44 | this.titleSerivce.showBackButton.next(true); 45 | } 46 | }); 47 | 48 | this.router.events 49 | .pipe( 50 | filter((event) => event instanceof NavigationEnd), 51 | map(() => { 52 | let child = this.activatedRoute.firstChild; 53 | while (child.firstChild) { 54 | child = child.firstChild; 55 | } 56 | if (child.snapshot.data['title']) { 57 | return child.snapshot.data['title']; 58 | } 59 | return null; 60 | }) 61 | ) 62 | .subscribe((ttl: string) => { 63 | this.titleSerivce.setTitle(ttl); 64 | }); 65 | 66 | this.CheckForUpdate(); 67 | } 68 | 69 | CheckForUpdate() { 70 | this.updates.available.subscribe(() => { 71 | this.snackBar 72 | .open('New Update Available', 'Refresh') 73 | .onAction() 74 | .subscribe(() => 75 | this.updates.activateUpdate().then(() => document.location.reload()) 76 | ); 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/src/assets/city.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/city.jpg -------------------------------------------------------------------------------- /src/src/assets/config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "FinanceApiUrl": { 6 | "type": "string" 7 | }, 8 | "IsDemo": { 9 | "type": "boolean", 10 | "default": false 11 | }, 12 | "SiteName": { 13 | "type": "string", 14 | "default": "Finance Manager" 15 | } 16 | }, 17 | "required": ["FinanceApiUrl"] 18 | } 19 | -------------------------------------------------------------------------------- /src/src/assets/defaultTransaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/defaultTransaction.png -------------------------------------------------------------------------------- /src/src/assets/demo.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./config.schema.json", 3 | "IsDemo": true, 4 | "FinanceApiUrl": "https://demo.finance.api.benfl3713.ddns.me/api" 5 | } 6 | -------------------------------------------------------------------------------- /src/src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/src/assets/logo_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/logo_square.png -------------------------------------------------------------------------------- /src/src/assets/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/assets/preview.png -------------------------------------------------------------------------------- /src/src/assets/template.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./config.schema.json", 3 | "FinanceApiUrl": "${FinanceApiUrl}" 4 | } 5 | -------------------------------------------------------------------------------- /src/src/default.theme.scss: -------------------------------------------------------------------------------- 1 | @import "~@angular/material/theming"; 2 | // Plus imports for other components in your app. 3 | 4 | // Include the common styles for Angular Material. We include this here so that you only 5 | // have to load a single css file for Angular Material in your app. 6 | // **Be sure that you only ever include this mixin once!** 7 | @include mat-core(); 8 | 9 | // Define the default theme (same as the example above). 10 | $finance-primary: mat-palette($mat-blue, A700); 11 | $finance-accent: mat-palette($mat-pink, A200, A100, A400); 12 | $finance-warn: mat-palette($mat-deep-orange); 13 | $finance-theme: mat-light-theme( 14 | $finance-primary, 15 | $finance-accent, 16 | $finance-warn 17 | ); 18 | 19 | table a { 20 | color: black; 21 | } 22 | // Include the default theme styles. 23 | @include angular-material-theme($finance-theme); 24 | 25 | // Define an alternate dark theme. 26 | $dark-primary: mat-palette($mat-blue, A700); 27 | $dark-accent: mat-palette($mat-pink, A200, A100, A400); 28 | $dark-warn: mat-palette($mat-deep-orange); 29 | $dark-theme: mat-dark-theme($dark-primary, $dark-accent, $dark-warn); 30 | 31 | // Include the alternative theme styles inside of a block with a CSS class. You can make this 32 | // CSS class whatever you want. In this example, any component inside of an element with 33 | // `.unicorn-dark-theme` will be affected by this alternate dark theme instead of the default theme. 34 | .finance-dark-theme { 35 | @include angular-material-theme($dark-theme); 36 | 37 | table a { 38 | color: white; 39 | } 40 | } 41 | 42 | .mat-primary-background { 43 | background-color: mat-color($mat-blue, A700); 44 | color: white !important; 45 | } 46 | 47 | mat-nav-list .menu-item:focus { 48 | background-color: mat-color($mat-blue, A700) !important; 49 | color: white !important; 50 | } 51 | 52 | mat-nav-list .menu-item:hover { 53 | color: mat-color($mat-blue, A700) !important; 54 | background-color: white; 55 | } 56 | -------------------------------------------------------------------------------- /src/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benfl3713/Finance-Manager/830f96f5edbfad16cf0e151f23e8eeb48377df42/src/src/favicon.ico -------------------------------------------------------------------------------- /src/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Finance Manager 6 | 7 | 8 | 9 | 15 | 19 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Finance Manager", 3 | "short_name": "Finance Manager", 4 | "theme_color": "#2962ff", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "assets/icons/icon-96x96.png", 18 | "sizes": "96x96", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | }, 22 | { 23 | "src": "assets/icons/icon-128x128.png", 24 | "sizes": "128x128", 25 | "type": "image/png", 26 | "purpose": "maskable any" 27 | }, 28 | { 29 | "src": "assets/icons/icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "purpose": "maskable any" 33 | }, 34 | { 35 | "src": "assets/icons/icon-152x152.png", 36 | "sizes": "152x152", 37 | "type": "image/png", 38 | "purpose": "maskable any" 39 | }, 40 | { 41 | "src": "assets/icons/icon-192x192.png", 42 | "sizes": "192x192", 43 | "type": "image/png", 44 | "purpose": "maskable any" 45 | }, 46 | { 47 | "src": "assets/icons/icon-384x384.png", 48 | "sizes": "384x384", 49 | "type": "image/png", 50 | "purpose": "maskable any" 51 | }, 52 | { 53 | "src": "assets/icons/icon-512x512.png", 54 | "sizes": "512x512", 55 | "type": "image/png", 56 | "purpose": "maskable any" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import 'ag-grid-community/dist/styles/ag-grid.css'; 3 | @import 'ag-grid-community/dist/styles/ag-theme-alpine.css'; 4 | @import 'ag-grid-community/dist/styles/ag-theme-balham.css'; 5 | @import 'ag-grid-community/dist/styles/ag-theme-material.css'; 6 | @import 'ag-grid-community/dist/styles/ag-theme-alpine-dark.css'; 7 | @import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css'; 8 | 9 | html, 10 | body { 11 | height: 100%; 12 | } 13 | body { 14 | margin: 0; 15 | font-family: Roboto, "Helvetica Neue", sans-serif; 16 | } 17 | 18 | button:disabled { 19 | cursor: not-allowed; 20 | } 21 | 22 | .transaction-logo { 23 | border-radius: 50%; 24 | height: 30px; 25 | width: 30px; 26 | } 27 | 28 | .clickable { 29 | cursor: pointer; 30 | } 31 | 32 | .centre-text { 33 | text-align: center; 34 | } 35 | 36 | .hide-button-outline:focus { 37 | outline: none; 38 | box-shadow: none; 39 | } 40 | 41 | .mobileChart { 42 | min-height: 400px; 43 | } 44 | 45 | .outline { 46 | border: 1px solid rgba(0, 0, 0, 0.2); 47 | } 48 | 49 | .buy-text { 50 | color: forestgreen; 51 | } 52 | 53 | .sale-text { 54 | color: red; 55 | } 56 | -------------------------------------------------------------------------------- /src/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "enableI18nLegacyMessageIdFormat": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------