├── frontend
├── src
│ ├── index.css
│ ├── vite-env.d.ts
│ ├── model
│ │ ├── Category.ts
│ │ ├── CostType.ts
│ │ ├── Interval.ts
│ │ ├── FinanceReport.ts
│ │ ├── EntryWithNoId.ts
│ │ ├── Entry.ts
│ │ └── MonthlyBalance.ts
│ ├── main.tsx
│ ├── entriesList
│ │ └── EntriesList.tsx
│ ├── App.tsx
│ ├── navBar
│ │ └── LongMenu.tsx
│ ├── financeReport
│ │ └── FinanceReport.tsx
│ ├── hooks
│ │ └── useStore.ts
│ ├── monthlyBalance
│ │ └── MonthlyBalance.tsx
│ ├── home
│ │ └── Home.tsx
│ ├── financeReportCard
│ │ └── FinanceReportCard.tsx
│ ├── monthlyBalanceAmountsCard
│ │ └── MonthlyBalanceAmountsCard.tsx
│ ├── entryCard
│ │ └── EntryCard.tsx
│ └── entryAddUpdate
│ │ └── EntryAddUpdate.tsx
├── tsconfig.node.json
├── .gitignore
├── index.html
├── vite.config.ts
├── .eslintrc.cjs
├── tsconfig.json
└── package.json
├── .gitignore
├── backend
├── lombok.config
├── src
│ ├── main
│ │ ├── resources
│ │ │ └── application.properties
│ │ └── java
│ │ │ └── de
│ │ │ └── neuefische
│ │ │ └── capstone
│ │ │ └── backend
│ │ │ ├── model
│ │ │ ├── CostType.java
│ │ │ ├── Category.java
│ │ │ ├── EntriesSortByInterval.java
│ │ │ ├── MonthlyEntries.java
│ │ │ ├── Interval.java
│ │ │ ├── FinanceReport.java
│ │ │ ├── EntryWithNoId.java
│ │ │ ├── MonthlyBalance.java
│ │ │ └── Entry.java
│ │ │ ├── entries
│ │ │ ├── IdService.java
│ │ │ ├── EntriesRepo.java
│ │ │ ├── EntriesController.java
│ │ │ └── EntriesService.java
│ │ │ ├── BackendApplication.java
│ │ │ ├── financereport
│ │ │ ├── FinanceReportService.java
│ │ │ ├── FinanceReportController.java
│ │ │ └── FinanceReportCalculate.java
│ │ │ └── monthlybalance
│ │ │ ├── MonthlyBalanceService.java
│ │ │ ├── MonthlyBalanceController.java
│ │ │ ├── MonthlyBalanceCalculate.java
│ │ │ └── MonthlySort.java
│ ├── lombok.config
│ └── test
│ │ ├── resources
│ │ └── application.properties
│ │ └── java
│ │ └── de
│ │ └── neuefische
│ │ └── capstone
│ │ └── backend
│ │ ├── BackendApplicationTests.java
│ │ ├── model
│ │ └── IntervalTest.java
│ │ ├── monthlybalance
│ │ ├── MonthlyBalanceControllerTest.java
│ │ ├── MonthlyBalanceCalculateTest.java
│ │ ├── MonthlyBalanceServiceTest.java
│ │ └── MonthlySortTest.java
│ │ ├── financereport
│ │ ├── FinanceReportCalculateTest.java
│ │ ├── FinanceReportServiceTest.java
│ │ └── FinanceReportIntegrationTest.java
│ │ └── entries
│ │ ├── EntriesServiceTest.java
│ │ └── EntriesIntegrationTest.java
├── .mvn
│ └── wrapper
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
├── .gitignore
├── pom.xml
├── mvnw.cmd
└── mvnw
├── Dockerfile
├── frontend.iml
├── capstone-budgetary-control.iml
├── sonar-project.properties
├── .github
└── workflows
│ ├── sonar-frontend.yml
│ ├── maven.yml
│ ├── show-logs.yml
│ ├── sonar-backend.yml
│ └── deploy.yml
└── README.md
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | *.iml
3 |
--------------------------------------------------------------------------------
/backend/lombok.config:
--------------------------------------------------------------------------------
1 | lombok.addLombokGeneratedAnnotation = true
--------------------------------------------------------------------------------
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/src/model/Category.ts:
--------------------------------------------------------------------------------
1 | export type Category = "INCOME" | "EXPENSE";
--------------------------------------------------------------------------------
/frontend/src/model/CostType.ts:
--------------------------------------------------------------------------------
1 | export type CostType = "FIXED" | "VARIABLE"
--------------------------------------------------------------------------------
/backend/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 |
2 | spring.data.mongodb.uri=${MONGO_DB_URI}
--------------------------------------------------------------------------------
/backend/src/lombok.config:
--------------------------------------------------------------------------------
1 |
2 | config.stopBubbling = true
3 |
4 | lombok.addLombokGeneratedAnnotation = true
--------------------------------------------------------------------------------
/backend/src/test/resources/application.properties:
--------------------------------------------------------------------------------
1 | de.flapdoodle.mongodb.embedded.version=6.0.1
2 | server.port=8081
--------------------------------------------------------------------------------
/frontend/src/model/Interval.ts:
--------------------------------------------------------------------------------
1 | export type Interval = "ONCE" | "MONTHLY" | "QUARTERLY" | "HALF_YEARLY" | "YEARLY";
--------------------------------------------------------------------------------
/backend/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kajochi/capstone-budgetary-control/HEAD/backend/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/model/CostType.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.model;
2 |
3 | public enum CostType {
4 | FIXED,
5 | VARIABLE
6 | }
7 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/model/Category.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.model;
2 |
3 | public enum Category {
4 |
5 | INCOME,
6 | EXPENSE
7 | }
8 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:20
2 |
3 | ENV ENVIRONMENT=prod
4 |
5 | LABEL maintainer="christoph.groneberg@neuefische.de"
6 |
7 | EXPOSE 8080
8 |
9 | ADD backend/target/budgetaryControl.jar app.jar
10 |
11 | CMD [ "sh", "-c", "java -jar /app.jar" ]
--------------------------------------------------------------------------------
/backend/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
3 |
--------------------------------------------------------------------------------
/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/model/FinanceReport.ts:
--------------------------------------------------------------------------------
1 |
2 | import {Interval} from "./Interval.ts";
3 |
4 | export type FinanceReport = {
5 |
6 | period: Interval,
7 | totalIncome: string,
8 | totalExpenses: string,
9 | fixCosts: string,
10 | variableCosts: string,
11 | balance: string,
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/frontend.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/capstone-budgetary-control.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/entries/IdService.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.entries;
2 |
3 | import org.springframework.stereotype.Component;
4 |
5 | @Component
6 | public class IdService {
7 |
8 | public String createRandomId() {
9 | return java.util.UUID.randomUUID().toString();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/src/model/EntryWithNoId.ts:
--------------------------------------------------------------------------------
1 | import {Interval} from "./Interval.ts";
2 | import {Category} from "./Category.ts";
3 | import {CostType} from "./CostType.ts";
4 |
5 | export type EntryWithNoId = {
6 | title: string;
7 | description: string;
8 | date: string;
9 | amount: string;
10 | interval: Interval;
11 | category: Category;
12 | costType: CostType;
13 | }
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | host: 'localhost',
9 | port: 3000,
10 | proxy: {
11 | '/api/': {
12 | target: 'http://localhost:8080',
13 | }
14 | }
15 | }
16 | })
--------------------------------------------------------------------------------
/frontend/src/model/Entry.ts:
--------------------------------------------------------------------------------
1 | import {Category} from "./Category.ts";
2 | import {Interval} from "./Interval.ts";
3 | import {CostType} from "./CostType.ts";
4 |
5 | export type Entry = {
6 | id: string;
7 | title: string;
8 | date: string;
9 | description: string;
10 | amount: string;
11 | category: Category;
12 | interval: Interval;
13 | costType: CostType
14 | }
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/model/EntriesSortByInterval.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.model;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 |
6 | import java.util.List;
7 |
8 | @Data
9 | @AllArgsConstructor
10 | public class EntriesSortByInterval {
11 |
12 | private final Interval interval;
13 |
14 | List entries;
15 | }
16 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/model/MonthlyEntries.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.model;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 |
6 | import java.util.List;
7 |
8 | @Data
9 | @AllArgsConstructor
10 | public class MonthlyEntries {
11 |
12 | private final int year;
13 | private final int month;
14 | private final List entries;
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 | import {BrowserRouter} from "react-router-dom";
6 |
7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
8 |
9 |
10 |
11 |
12 | ,
13 | )
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/BackendApplication.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class BackendApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(BackendApplication.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/BackendApplicationTests.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.springframework.boot.test.context.SpringBootTest;
5 |
6 | import static org.junit.jupiter.api.Assertions.assertTrue;
7 |
8 | @SpringBootTest
9 | class BackendApplicationTests {
10 |
11 | @Test
12 | void contextLoads() {
13 | assertTrue(true);
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { browser: true, es2020: true },
3 | extends: [
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:react-hooks/recommended',
7 | ],
8 | parser: '@typescript-eslint/parser',
9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
10 | plugins: ['react-refresh'],
11 | rules: {
12 | 'react-refresh/only-export-components': 'warn',
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/entries/EntriesRepo.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.entries;
2 |
3 | import de.neuefische.capstone.backend.model.Entry;
4 | import org.springframework.data.mongodb.repository.MongoRepository;
5 |
6 | import org.springframework.stereotype.Repository;
7 |
8 |
9 |
10 | @Repository
11 | public interface EntriesRepo extends MongoRepository {
12 |
13 |
14 | Entry findFirstByOrderByDateAsc();
15 | }
16 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=kajochi_capstone-budgetary-control-frontend
2 | sonar.organization=kajochi
3 |
4 | # This is the name and version displayed in the SonarCloud UI.
5 | #sonar.projectName=kajochi_capstone-budgetary-control-frontend
6 | #sonar.projectVersion=1.0
7 |
8 |
9 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
10 | sonar.sources=./frontend/src
11 |
12 | # Encoding of the source code. Default is default system encoding
13 | #sonar.sourceEncoding=UTF-8
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/frontend/src/model/MonthlyBalance.ts:
--------------------------------------------------------------------------------
1 | import {Entry} from "./Entry.ts";
2 |
3 | export type MonthlyBalance = {
4 | yearMonth: string;
5 | totalIncome: string;
6 | totalExpenses: string;
7 | fixedCosts: string;
8 | variableCosts: string;
9 | oneTimeCosts: string;
10 | balance: string;
11 | monthlyEntries: Entry [];
12 | }
13 |
14 | export type MonthlyBalanceAmounts = {
15 | totalIncome: string;
16 | totalExpense: string;
17 | fixedCosts: string;
18 | variableCosts: string;
19 | oneTimeCosts: string;
20 | balance: string;
21 | }
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/model/Interval.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.model;
2 |
3 | public enum Interval {
4 |
5 | MONTHLY,
6 | QUARTERLY,
7 | HALF_YEARLY,
8 | YEARLY,
9 | ONCE;
10 |
11 |
12 | public static int getMultiplier(Interval interval) {
13 | return switch (interval) {
14 | case MONTHLY -> 1;
15 | case QUARTERLY -> 3;
16 | case HALF_YEARLY -> 6;
17 | case YEARLY -> 12;
18 | case ONCE -> 0;
19 | };
20 |
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/model/FinanceReport.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.model;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 |
6 | import java.math.BigDecimal;
7 |
8 | @Data
9 | @AllArgsConstructor
10 | public class FinanceReport {
11 |
12 | private final Interval period;
13 | private final BigDecimal totalIncome;
14 | private final BigDecimal totalExpenses;
15 | private final BigDecimal fixCosts;
16 | private final BigDecimal variableCosts;
17 | private final BigDecimal balance;
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/model/EntryWithNoId.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.model;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 |
6 | import java.math.BigDecimal;
7 | import java.time.LocalDate;
8 |
9 | @Data
10 | @AllArgsConstructor
11 | public class EntryWithNoId {
12 |
13 | private final String title;
14 | private final String description;
15 | private final LocalDate date;
16 | private final BigDecimal amount;
17 | private final Category category;
18 | private final Interval interval;
19 | private final CostType costType;
20 | }
21 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/model/MonthlyBalance.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.model;
2 |
3 | import lombok.Data;
4 |
5 | import java.math.BigDecimal;
6 | import java.util.List;
7 |
8 | @Data
9 | public class MonthlyBalance {
10 |
11 | private final String monthYear;
12 | private final BigDecimal totalIncome;
13 | private final BigDecimal totalExpenses;
14 | private final BigDecimal fixedCosts;
15 | private final BigDecimal variableCosts;
16 | private final BigDecimal oneTimeCosts;
17 | private final BigDecimal balance;
18 |
19 | private final List monthlyEntries;
20 | }
21 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/financereport/FinanceReportService.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.financereport;
2 |
3 | import de.neuefische.capstone.backend.model.FinanceReport;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Data;
6 | import org.springframework.stereotype.Service;
7 |
8 | import java.util.List;
9 |
10 | @Service
11 | @Data
12 | @AllArgsConstructor
13 | public class FinanceReportService {
14 |
15 | private final FinanceReportCalculate financeReportCalculate;
16 |
17 | public List getFinanceReports() {
18 | return financeReportCalculate.calculateFinanceReports();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/sonar-frontend.yml:
--------------------------------------------------------------------------------
1 | name: Sonar-Frontend
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | types: [opened, synchronize, reopened]
8 | jobs:
9 | sonarcloud:
10 | name: SonarCloud
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | with:
15 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
16 | - name: SonarCloud Scan
17 | uses: SonarSource/sonarcloud-github-action@master
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
20 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/monthlybalance/MonthlyBalanceService.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.monthlybalance;
2 |
3 | import de.neuefische.capstone.backend.model.MonthlyBalance;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Data;
6 | import org.springframework.stereotype.Service;
7 |
8 | import java.util.Map;
9 |
10 | @Service
11 | @Data
12 | @AllArgsConstructor
13 | public class MonthlyBalanceService {
14 |
15 | private final MonthlySort monthlySort;
16 |
17 | public Map getMonthlyBalanceList() {
18 | return monthlySort.generateMonthlyBalanceList();
19 | }
20 |
21 |
22 |
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # capstone-budgetary-control
2 |
3 | [](https://sonarcloud.io/summary/new_code?id=kajochi_capstone-budgetary-control-frontend)
4 | [](https://sonarcloud.io/summary/new_code?id=kajochi_capstone-budgetary-control-backend)
5 | [](https://sonarcloud.io/summary/new_code?id=kajochi_capstone-budgetary-control-frontend)
6 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/model/Entry.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.model;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 | import org.springframework.data.annotation.Id;
6 | import org.springframework.data.mongodb.core.mapping.Document;
7 |
8 | import java.math.BigDecimal;
9 | import java.time.LocalDate;
10 |
11 | @Data
12 | @AllArgsConstructor
13 | @Document("entries")
14 | public class Entry {
15 | @Id
16 | private final String id;
17 |
18 | private final String title;
19 |
20 | private final String description;
21 |
22 | private final LocalDate date;
23 |
24 | private final BigDecimal amount;
25 |
26 | private final Category category;
27 |
28 | private final Interval interval;
29 |
30 | private final CostType costType;
31 | }
32 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/model/IntervalTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.model;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static org.junit.jupiter.api.Assertions.*;
6 |
7 | class IntervalTest {
8 |
9 | @Test
10 | void getMultiplierShouldReturn1ForMonthly() {
11 | //GIVEN
12 | Interval interval = Interval.MONTHLY;
13 | //WHEN
14 | int actual = Interval.getMultiplier(interval);
15 | //THEN
16 | assertEquals(1, actual);
17 | }
18 |
19 | @Test
20 | void getMultiplierShouldReturn0ForOnce() {
21 | //GIVEN
22 | Interval interval = Interval.ONCE;
23 | //WHEN
24 | int actual = Interval.getMultiplier(interval);
25 | //THEN
26 | assertEquals(0, actual);
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/financereport/FinanceReportController.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.financereport;
2 |
3 | import de.neuefische.capstone.backend.model.FinanceReport;
4 | import org.springframework.web.bind.annotation.GetMapping;
5 | import org.springframework.web.bind.annotation.RequestMapping;
6 | import org.springframework.web.bind.annotation.RestController;
7 |
8 | import java.util.List;
9 |
10 | @RestController
11 | @RequestMapping("/api/financeReports")
12 | public class FinanceReportController {
13 |
14 | private final FinanceReportService financeReportService;
15 |
16 | FinanceReportController(FinanceReportService financeReportService) {
17 | this.financeReportService = financeReportService;
18 | }
19 | @GetMapping
20 | List getFinanceReports() {
21 | return financeReportService.getFinanceReports();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Java CI with Maven
10 |
11 | on:
12 | push:
13 |
14 | jobs:
15 | build:
16 |
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: Set up JDK 20
22 | uses: actions/setup-java@v3
23 | with:
24 | java-version: '20'
25 | distribution: 'temurin'
26 | cache: maven
27 | - name: Build with Maven
28 | run: mvn -B package --file backend/pom.xml
29 |
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/monthlybalance/MonthlyBalanceController.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.monthlybalance;
2 |
3 | import de.neuefische.capstone.backend.model.MonthlyBalance;
4 | import org.springframework.web.bind.annotation.GetMapping;
5 | import org.springframework.web.bind.annotation.RequestMapping;
6 | import org.springframework.web.bind.annotation.RestController;
7 |
8 | import java.util.Map;
9 |
10 | @RestController
11 | @RequestMapping("/api/monthlybalance")
12 | public class MonthlyBalanceController {
13 |
14 | private final MonthlyBalanceService monthlyBalanceService;
15 |
16 | MonthlyBalanceController (MonthlyBalanceService monthlyBalanceService) {
17 | this.monthlyBalanceService = monthlyBalanceService;
18 | }
19 |
20 | @GetMapping
21 | Map getMonthlyBalanceList() {
22 | return monthlyBalanceService.getMonthlyBalanceList();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.github/workflows/show-logs.yml:
--------------------------------------------------------------------------------
1 |
2 | name: "Get Logs"
3 |
4 | on:
5 | workflow_dispatch:
6 |
7 | jobs:
8 | get-logs:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Get logs from docker
12 | uses: appleboy/ssh-action@master
13 | with:
14 | host: capstone-project.de
15 | #Set App Name (replace "example" with "alpha"-"tango")
16 | username: cgn-java-23-2-christoph
17 | password: ${{ secrets.SSH_PASSWORD }}
18 | #Set App Name (replace "example" with "alpha"-"tango")
19 | script: |
20 | sudo docker logs cgn-java-23-2-christoph
21 | - name: Check the deployed service URL
22 | uses: jtalk/url-health-check-action@v3
23 | with:
24 | #Set App Name (replace "example" with "alpha"-"tango")
25 | url: http://cgn-java-23-2-christoph.capstone-project.de
26 | max-attempts: 3
27 | retry-delay: 5s
28 | retry-all: true
29 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/entries/EntriesController.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.entries;
2 |
3 | import de.neuefische.capstone.backend.model.Entry;
4 | import de.neuefische.capstone.backend.model.EntryWithNoId;
5 | import org.springframework.web.bind.annotation.*;
6 |
7 | import java.util.List;
8 |
9 | @RestController
10 | @RequestMapping("/api/entries")
11 | public class EntriesController {
12 |
13 | private final EntriesService entriesService;
14 |
15 | EntriesController(EntriesService entriesService) {
16 | this.entriesService = entriesService;
17 | }
18 | @GetMapping
19 | List getEntries() {
20 | return entriesService.getAllEntries();
21 | }
22 |
23 | @PostMapping
24 | Entry addEntry(@RequestBody EntryWithNoId entryWithNoId) {
25 | return entriesService.addEntry(entryWithNoId);
26 | }
27 |
28 | @PutMapping("{id}")
29 | Entry updateEntry(@RequestBody EntryWithNoId entryWithNoId, @PathVariable String id) {
30 | return entriesService.updateEntry(entryWithNoId, id);
31 | }
32 |
33 | @DeleteMapping("{id}")
34 | void deleteEntry(@PathVariable String id) {
35 | entriesService.deleteEntry(id);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@emotion/react": "^11.11.1",
14 | "@emotion/styled": "^11.11.0",
15 | "@material/card": "^14.0.0",
16 | "@material/typography": "^14.0.0",
17 | "@mui/icons-material": "^5.14.1",
18 | "@mui/material": "^5.14.2",
19 | "axios": "^1.4.0",
20 | "browser-router": "^0.2.0",
21 | "dom": "^0.0.3",
22 | "moment": "^2.29.4",
23 | "react": "^18.2.0",
24 | "react-dom": "^18.2.0",
25 | "react-router-dom": "^6.15.0",
26 | "router": "^1.3.8",
27 | "zustand": "^4.3.9"
28 | },
29 | "devDependencies": {
30 | "@types/react": "^18.0.37",
31 | "@types/react-dom": "^18.0.11",
32 | "@typescript-eslint/eslint-plugin": "^5.59.0",
33 | "@typescript-eslint/parser": "^5.59.0",
34 | "@vitejs/plugin-react": "^4.0.0",
35 | "eslint": "^8.38.0",
36 | "eslint-plugin-react-hooks": "^4.6.0",
37 | "eslint-plugin-react-refresh": "^0.3.4",
38 | "typescript": "^5.0.2",
39 | "vite": "^4.3.9"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.github/workflows/sonar-backend.yml:
--------------------------------------------------------------------------------
1 | name: SonarCloud-Backend
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | types: [opened, synchronize, reopened]
8 | jobs:
9 | build:
10 | name: Build and analyze
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | with:
15 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
16 | - name: Set up JDK 20
17 | uses: actions/setup-java@v3
18 | with:
19 | java-version: 20
20 | distribution: 'zulu' # Alternative distribution options are available.
21 | - name: Cache SonarCloud packages
22 | uses: actions/cache@v3
23 | with:
24 | path: ~/.sonar/cache
25 | key: ${{ runner.os }}-sonar
26 | restore-keys: ${{ runner.os }}-sonar
27 | - name: Cache Maven packages
28 | uses: actions/cache@v3
29 | with:
30 | path: ~/.m2
31 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
32 | restore-keys: ${{ runner.os }}-m2
33 | - name: Build and analyze
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
36 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
37 | run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=kajochi_capstone-budgetary-control-backend --file backend/pom.xml
--------------------------------------------------------------------------------
/frontend/src/entriesList/EntriesList.tsx:
--------------------------------------------------------------------------------
1 | import {Entry} from "../model/Entry.ts";
2 | import EntryCard from "../entryCard/EntryCard.tsx";
3 | import {useStore} from "../hooks/useStore.ts";
4 | import {useEffect} from "react";
5 | import styled from "@emotion/styled";
6 | import MonthlyBalanceAmountsCard from "../monthlyBalanceAmountsCard/MonthlyBalanceAmountsCard.tsx";
7 |
8 |
9 | type Props = {
10 | monthYear: string;
11 | }
12 |
13 | export default function EntriesList(props: Props) {
14 |
15 |
16 | const monthlyBalances = useStore((state) => state.monthlyBalances)
17 | const getMonthlyBalances = useStore((state) => state.getMonthlyBalances)
18 |
19 |
20 | useEffect(() => {
21 |
22 | getMonthlyBalances()
23 | }, [getMonthlyBalances])
24 |
25 |
26 | return (
27 | <>
28 |
29 | {
30 | monthlyBalances?.[props.monthYear]?.monthlyEntries ? (
31 | <>
32 |
33 | {monthlyBalances?.[props.monthYear]?.monthlyEntries.map((entry: Entry) => {
34 | return
35 | })}
36 | >
37 | ) : (
38 | There are no entries for this month
39 | )
40 | }
41 |
42 | >
43 |
44 | )
45 | }
46 | const StyledP = styled.p`
47 | font-family: "Roboto Light", sans-serif;
48 | display: flex;
49 | justify-content: center;
50 | `;
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import styled from "@emotion/styled";
2 | import {Route, Routes} from "react-router-dom";
3 | import EntryAddUpdate from "./entryAddUpdate/EntryAddUpdate.tsx";
4 | import Home from "./home/Home.tsx";
5 | import LongMenu from "./navBar/LongMenu.tsx";
6 | import SavingsIcon from '@mui/icons-material/Savings';
7 | import MonthlyBalance from "./monthlyBalance/MonthlyBalance.tsx";
8 | import FinanceReport from "./financeReport/FinanceReport.tsx";
9 |
10 | export default function App() {
11 |
12 |
13 |
14 | return (
15 | <>
16 |
17 |
18 |
19 |
20 | Budgetary Control
21 |
22 |
23 |
24 |
25 |
26 | }/>
27 | }/>
28 | }/>
29 | }/>
30 |
31 |
32 | >
33 |
34 | )
35 | }
36 |
37 | const StyledH1 = styled.h2`
38 | font-family: "Roboto Light", sans-serif;
39 | color: #ffffff;
40 | justify-content: center;
41 | width: 100%;
42 | margin-left: 5px;
43 | `;
44 |
45 | const StyledDiv = styled.div`
46 | display: flex;
47 | background-color: #4d6bdd;
48 | align-items: center;
49 | width: 100%;
50 | margin-top: 15px;
51 | border-radius: 7px;
52 | box-shadow: 10px 10px 5px -4px silver;
53 | `;
54 | const HeaderChildDiv1 = styled.div`
55 | justify-content: flex-start;
56 | margin-left: 10px;
57 |
58 | `;
59 |
60 |
61 | const HeaderChildDiv3 = styled.div`
62 | justify-content: flex-end;
63 | margin-right: 10px;
64 |
65 | `;
66 |
67 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/entries/EntriesService.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.entries;
2 |
3 | import de.neuefische.capstone.backend.model.Entry;
4 | import de.neuefische.capstone.backend.model.EntryWithNoId;
5 | import org.springframework.stereotype.Service;
6 |
7 | import java.util.List;
8 | @Service
9 | public class EntriesService {
10 |
11 | private final EntriesRepo entriesRepo;
12 |
13 | private final IdService idService;
14 |
15 | public EntriesService(EntriesRepo entriesRepo, IdService idService) {
16 | this.idService = idService;
17 | this.entriesRepo = entriesRepo;
18 | }
19 |
20 | public List getAllEntries() {
21 | return entriesRepo.findAll();
22 | }
23 |
24 | public Entry addEntry(EntryWithNoId entryWithNoId) {
25 | return entriesRepo.insert(new Entry(
26 | idService.createRandomId(),
27 | entryWithNoId.getTitle(),
28 | entryWithNoId.getDescription(),
29 | entryWithNoId.getDate(),
30 | entryWithNoId.getAmount(),
31 | entryWithNoId.getCategory(),
32 | entryWithNoId.getInterval(),
33 | entryWithNoId.getCostType()
34 | ));
35 | }
36 |
37 | public Entry updateEntry(EntryWithNoId entryWithNoId, String id) {
38 | Entry updatedEntry = new Entry(
39 | id,
40 | entryWithNoId.getTitle(),
41 | entryWithNoId.getDescription(),
42 | entryWithNoId.getDate(),
43 | entryWithNoId.getAmount(),
44 | entryWithNoId.getCategory(),
45 | entryWithNoId.getInterval(),
46 | entryWithNoId.getCostType()
47 | );
48 |
49 | return entriesRepo.save(updatedEntry);
50 | }
51 |
52 | public void deleteEntry(String id) {
53 | if (!entriesRepo.existsById(id)) throw new IllegalArgumentException();
54 | entriesRepo.deleteById(id);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/monthlybalance/MonthlyBalanceCalculate.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.monthlybalance;
2 |
3 | import de.neuefische.capstone.backend.model.Category;
4 | import de.neuefische.capstone.backend.model.CostType;
5 | import de.neuefische.capstone.backend.model.Entry;
6 | import de.neuefische.capstone.backend.model.Interval;
7 | import org.springframework.stereotype.Component;
8 |
9 | import java.math.BigDecimal;
10 |
11 | import java.util.List;
12 | @Component
13 | public class MonthlyBalanceCalculate {
14 |
15 | private MonthlyBalanceCalculate() {
16 | }
17 | public static List calculateEntryAmounts(List entriesSortedByStartDate) {
18 |
19 |
20 | BigDecimal totalIncome = BigDecimal.ZERO;
21 | BigDecimal totalExpenses;
22 | BigDecimal fixedCosts = BigDecimal.ZERO;
23 | BigDecimal variableCosts = BigDecimal.ZERO;
24 | BigDecimal oneTimeCosts = BigDecimal.ZERO;
25 |
26 |
27 | for (Entry entry : entriesSortedByStartDate) {
28 |
29 | if (entry.getCostType().equals(CostType.FIXED)) {
30 | if (entry.getCategory().equals(Category.INCOME)){
31 | totalIncome = totalIncome.add(entry.getAmount());
32 | }else{
33 | fixedCosts = fixedCosts.add(entry.getAmount());
34 | }
35 |
36 | }else {
37 | if (entry.getCategory().equals(Category.INCOME)){
38 | totalIncome = totalIncome.add(entry.getAmount());
39 | }else{
40 | variableCosts = variableCosts.add(entry.getAmount());
41 | }
42 | }
43 |
44 | if (entry.getInterval().equals(Interval.ONCE) && entry.getCategory().equals(Category.EXPENSE)) {
45 | oneTimeCosts = oneTimeCosts.add(entry.getAmount());
46 | }
47 | }
48 |
49 |
50 | totalExpenses = fixedCosts.add(variableCosts);
51 | BigDecimal balance = totalIncome.subtract(totalExpenses);
52 |
53 | return List.of(totalIncome, totalExpenses, fixedCosts, variableCosts, oneTimeCosts, balance);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/monthlybalance/MonthlyBalanceControllerTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.monthlybalance;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import de.neuefische.capstone.backend.model.*;
5 | import org.junit.jupiter.api.Test;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
8 | import org.springframework.boot.test.context.SpringBootTest;
9 | import org.springframework.http.MediaType;
10 | import org.springframework.test.annotation.DirtiesContext;
11 | import org.springframework.test.web.servlet.MockMvc;
12 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
13 |
14 | import java.math.BigDecimal;
15 | import java.time.LocalDate;
16 | import static org.junit.jupiter.api.Assertions.*;
17 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
18 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
19 |
20 | @SpringBootTest
21 | @AutoConfigureMockMvc
22 | class MonthlyBalanceControllerTest {
23 | @Autowired
24 | MockMvc mockMvc;
25 | @Autowired
26 | MonthlyBalanceService monthlyBalanceService;
27 | @Autowired
28 | ObjectMapper objectMapper;
29 |
30 | @DirtiesContext
31 | @Test
32 | void getMonthlyBalanceListWhenOneEntryIsAdded() throws Exception {
33 | //Given
34 | String jsonRequestBody = objectMapper.writeValueAsString(new EntryWithNoId(
35 | "testTitle",
36 | "testDescription",
37 | LocalDate.of(2023, 12, 3),
38 | new BigDecimal(1000),
39 | Category.INCOME,
40 | Interval.MONTHLY,
41 | CostType.FIXED
42 | ));
43 |
44 | //When
45 | mockMvc.perform(
46 | MockMvcRequestBuilders.post("/api/entries")
47 | .contentType(MediaType.APPLICATION_JSON)
48 | .content(jsonRequestBody))
49 | .andExpect(status().isOk());
50 |
51 | mockMvc.perform(
52 | MockMvcRequestBuilders.get("/api/monthlybalance"))
53 | .andExpect(status().isOk())
54 | .andExpect(content().contentType(MediaType.APPLICATION_JSON));
55 |
56 | //Then
57 | assertEquals(12, monthlyBalanceService.getMonthlyBalanceList().size());
58 |
59 | }
60 |
61 | }
--------------------------------------------------------------------------------
/frontend/src/navBar/LongMenu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import IconButton from '@mui/material/IconButton';
3 | import Menu from '@mui/material/Menu';
4 | import MenuItem from '@mui/material/MenuItem';
5 | import MenuIcon from '@mui/icons-material/Menu';
6 | import {useState} from "react";
7 | import {useNavigate} from "react-router-dom";
8 |
9 | const options = [
10 | 'Home',
11 | 'Monthly Balance',
12 | 'Finance Report',
13 | 'Add Entry',
14 |
15 | ];
16 |
17 |
18 |
19 |
20 | export default function LongMenu() {
21 | const [anchorEl, setAnchorEl] = useState(null);
22 | const open = Boolean(anchorEl);
23 |
24 | const navigate = useNavigate()
25 | const handleClick = (event: React.MouseEvent) => {
26 | setAnchorEl(event.currentTarget);
27 | };
28 | const handleClickOnItem = (event: React.MouseEvent) => {
29 | setAnchorEl(null);
30 | const item = event.currentTarget.textContent
31 | switch (item) {
32 | case "Home":
33 | navigate("/")
34 | break;
35 | case "Add Entry":
36 | navigate(`/add-entry`)
37 | break;
38 | case "Monthly Balance":
39 | navigate(`/monthlyBalance`)
40 | break;
41 | case "Finance Report":
42 | navigate("/financeReport")
43 | break;
44 | }
45 | };
46 |
47 | return (
48 |
49 |
57 |
58 |
59 |
75 |
76 | );
77 | }
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/financereport/FinanceReportCalculateTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.financereport;
2 |
3 | import de.neuefische.capstone.backend.entries.EntriesRepo;
4 | import de.neuefische.capstone.backend.model.*;
5 | import org.junit.jupiter.api.Test;
6 |
7 |
8 | import java.math.BigDecimal;
9 |
10 | import java.time.LocalDate;
11 | import java.util.List;
12 |
13 | import static org.junit.jupiter.api.Assertions.*;
14 | import static org.mockito.Mockito.*;
15 |
16 |
17 | class FinanceReportCalculateTest {
18 |
19 | EntriesRepo entriesRepo = mock(EntriesRepo.class);
20 |
21 |
22 | FinanceReportCalculate financeReportCalculate = new FinanceReportCalculate(entriesRepo);
23 | @Test
24 | void WhenPeriodMonthIsGivenCalculateFinanceReports(){
25 | //Given
26 | List filteredEntries = List.of(
27 | new Entry("1",
28 | "testTitle",
29 | "testDescription",
30 | LocalDate.of(2023, 12, 3),
31 | new BigDecimal(1000),
32 | Category.INCOME,
33 | Interval.MONTHLY,
34 | CostType.FIXED),
35 | new Entry("2",
36 | "testTitle",
37 | "testDescription",
38 | LocalDate.of(2023, 12, 3),
39 | new BigDecimal(500),
40 | Category.EXPENSE,
41 | Interval.MONTHLY,
42 | CostType.FIXED),
43 | new Entry("3",
44 | "testTitle",
45 | "testDescription",
46 | LocalDate.of(2023, 12, 3),
47 | new BigDecimal(200),
48 | Category.EXPENSE,
49 | Interval.MONTHLY,
50 | CostType.VARIABLE)
51 | );
52 | //When
53 | when(entriesRepo.findAll()).thenReturn(filteredEntries);
54 | FinanceReport expected = new FinanceReport(
55 | Interval.MONTHLY,
56 | new BigDecimal("1000.000"),
57 | new BigDecimal("700.000"),
58 | new BigDecimal("500.000"),
59 | new BigDecimal("200.000"),
60 | new BigDecimal("300.000")
61 | );
62 | FinanceReport actual = financeReportCalculate.calculateFinanceReports().get(0);
63 | //Then
64 | verify(entriesRepo).findAll();
65 | assertEquals(expected, actual);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/monthlybalance/MonthlyBalanceCalculateTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.monthlybalance;
2 |
3 | import de.neuefische.capstone.backend.model.Category;
4 | import de.neuefische.capstone.backend.model.CostType;
5 | import de.neuefische.capstone.backend.model.Entry;
6 | import de.neuefische.capstone.backend.model.Interval;
7 | import org.junit.jupiter.api.Test;
8 |
9 | import java.math.BigDecimal;
10 | import java.time.LocalDate;
11 | import java.util.List;
12 |
13 | import static org.junit.jupiter.api.Assertions.*;
14 |
15 | class MonthlyBalanceCalculateTest {
16 |
17 |
18 | @Test
19 | void calculateEntryAmountsWhenEntriesAreGiven() {
20 | //Given
21 | List entries = List.of(
22 | new Entry("1",
23 | "testTitle",
24 | "testDescription",
25 | LocalDate.of(2023, 12, 3),
26 | new BigDecimal(1000),
27 | Category.INCOME,
28 | Interval.MONTHLY,
29 | CostType.FIXED),
30 | new Entry("2",
31 | "testTitle",
32 | "testDescription",
33 | LocalDate.of(2023, 12, 3),
34 | new BigDecimal(500),
35 | Category.EXPENSE,
36 | Interval.MONTHLY,
37 | CostType.FIXED),
38 | new Entry("3",
39 | "testTitle",
40 | "testDescription",
41 | LocalDate.of(2023, 12, 3),
42 | new BigDecimal(200),
43 | Category.EXPENSE,
44 | Interval.MONTHLY,
45 | CostType.VARIABLE),
46 | new Entry("4",
47 | "testTitle",
48 | "testDescription",
49 | LocalDate.of(2023, 12, 3),
50 | new BigDecimal(100),
51 | Category.EXPENSE,
52 | Interval.ONCE,
53 | CostType.VARIABLE)
54 | );
55 | List expected = List.of(
56 | new BigDecimal("1000"),
57 | new BigDecimal("800"),
58 | new BigDecimal("500"),
59 | new BigDecimal("300"),
60 | new BigDecimal("100"),
61 | new BigDecimal("200"));
62 | //When
63 | List actual = MonthlyBalanceCalculate.calculateEntryAmounts(entries);
64 | //Then
65 | assertEquals(expected, actual);
66 | }
67 | }
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/monthlybalance/MonthlyBalanceServiceTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.monthlybalance;
2 |
3 | import de.neuefische.capstone.backend.model.*;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.math.BigDecimal;
7 | import java.time.LocalDate;
8 | import java.util.ArrayList;
9 | import java.util.HashMap;
10 | import java.util.List;
11 | import java.util.Map;
12 |
13 | import static org.assertj.core.api.Assertions.assertThat;
14 |
15 | import static org.mockito.Mockito.*;
16 |
17 | class MonthlyBalanceServiceTest {
18 | MonthlySort monthlySort = mock(MonthlySort.class);
19 | MonthlyBalanceService monthlyBalanceService = new MonthlyBalanceService(monthlySort);
20 | LocalDate currentDate = LocalDate.now();
21 | int currentMonth = currentDate.getMonthValue();
22 | int currentYear = currentDate.getYear();
23 | int monthInOneYear = currentMonth + 12;
24 | @Test
25 | void getMonthlyBalanceList() {
26 | //GIVEN
27 | List entries = List.of(
28 | new Entry("1",
29 | "testTitle",
30 | "testDescription",
31 | LocalDate.of(2023, 8, 22),
32 | new BigDecimal(1000),
33 | Category.INCOME,
34 | Interval.MONTHLY,
35 | CostType.FIXED));
36 | Map expected = new HashMap<>();
37 | for (int month = currentMonth; month < monthInOneYear; month++) {
38 | int magicMonth ;
39 | int year ;
40 | if (month <= 12) {
41 | magicMonth = month;
42 | year = currentYear;
43 | }else {
44 | magicMonth = month - 12;
45 | year = currentYear + 1;
46 | }
47 | String monthLabel = LocalDate.of(currentYear, magicMonth, 1).getMonth().toString().toUpperCase();
48 | monthLabel = monthLabel+"-"+year;
49 | MonthlyBalance monthlyBalance = new MonthlyBalance(monthLabel,
50 | new BigDecimal("1000"),
51 | new BigDecimal("0"),
52 | new BigDecimal("0"),
53 | new BigDecimal("0"),
54 | new BigDecimal("0"),
55 | new BigDecimal("1000"),
56 | new ArrayList<>(entries));
57 | expected.put(monthLabel, monthlyBalance);
58 | }
59 |
60 | //WHEN
61 | when(monthlySort.generateMonthlyBalanceList()).thenReturn(expected);
62 | Map actual = monthlyBalanceService.getMonthlyBalanceList();
63 | //THEN
64 | verify(monthlySort).generateMonthlyBalanceList();
65 | assertThat(actual).isEqualTo(expected);
66 | }
67 | }
--------------------------------------------------------------------------------
/frontend/src/financeReport/FinanceReport.tsx:
--------------------------------------------------------------------------------
1 | import styled from "@emotion/styled";
2 | import {FormControl, InputLabel, MenuItem, Select} from "@mui/material";
3 |
4 | import {useEffect, useState} from "react";
5 |
6 | import {useStore} from "../hooks/useStore.ts";
7 | import FinanceReportCard from "../financeReportCard/FinanceReportCard.tsx";
8 | import {Interval} from "../model/Interval.ts";
9 | import CalculateIcon from '@mui/icons-material/Calculate';
10 |
11 |
12 |
13 | export default function FinanceReport() {
14 |
15 | const [period, setPeriod] = useState('MONTHLY');
16 |
17 | const financeReports = useStore((state) => state.financeReports)
18 |
19 | const getFinanceReports = useStore((state) => state.getFinanceReports)
20 |
21 | useEffect(() => {
22 | getFinanceReports()
23 | }, [])
24 |
25 |
26 |
27 |
28 | return (
29 |
30 |
31 | Finance Report
32 |
33 |
34 |
35 | Here you can display your financial report for four different time periods.
36 | Recurring transactions are counted up or down. One-off costs are not included.
37 | This gives you an overview of your financial situation.
38 |
39 |
40 | Period
41 | setPeriod(e.target.value as Interval)}
44 | >
45 | Monthly
46 | Quarterly
47 | Half Yearly
48 | Yearly
49 |
50 |
51 |
52 |
53 | )
54 | }
55 |
56 | const StyledH2 = styled.h2`
57 | font-family: "Roboto Light", sans-serif;
58 | display: flex;
59 | justify-content: center;
60 | `;
61 |
62 | const StyledFormControl = styled(FormControl)`
63 | display: flex;
64 | justify-content: center;
65 | margin: 25px;
66 | `;
67 | const HeadingDiv = styled.div`
68 | display: flex;
69 | justify-content: center;
70 | align-items: center;
71 | `;
72 |
73 | const ExplonationText = styled.div`
74 | padding: 4px;
75 | border-radius: 7px;
76 | background-color: #edf0fc;
77 | font-family: "Roboto Light", sans-serif;
78 | display: flex;
79 | flex-direction: column;
80 | justify-content: center;
81 | margin-right: 20px;
82 | margin-left: 20px;
83 | margin-bottom: 25px;
84 | font-size: 16px;
85 | font-weight: 500;
86 | line-height: 1.6;
87 | letter-spacing: 0.0075em;
88 | text-align: center;
89 | box-shadow: 10px 10px 5px -4px silver;
90 | `;
91 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useStore.ts:
--------------------------------------------------------------------------------
1 | import {create} from "zustand";
2 | import axios from "axios";
3 | import {Entry} from "../model/Entry.ts";
4 | import {EntryWithNoId} from "../model/EntryWithNoId.ts";
5 | import {Simulate} from "react-dom/test-utils";
6 | import error = Simulate.error;
7 | import {FinanceReport} from "../model/FinanceReport.ts";
8 | import {MonthlyBalance} from "../model/MonthlyBalance.ts";
9 |
10 | type State = {
11 | entries: Entry[];
12 | financeReports: FinanceReport[];
13 | monthlyBalances: Record;
14 | selectedMonthYear: string | null;
15 | getEntries: () => void;
16 | getFinanceReports: () => void;
17 | getMonthlyBalances: () => void;
18 | createEntry: (requestBody: EntryWithNoId) => void;
19 | updateEntry: (requestBody: EntryWithNoId, id: string) => void;
20 | deleteEntry: (id: string) => void;
21 | isCardUpdated: boolean;
22 | updatedCardId: string;
23 | setIsCardUpdated: (updated: boolean) => void;
24 | setUpdatedCardId: (id: string) => void;
25 | getIsCardUpdated: () => boolean;
26 | getUpdatedCard: () => Entry | undefined;
27 | setSelectedMonthYear: (monthYear: string | null) => void;
28 |
29 |
30 | }
31 |
32 |
33 | export const useStore = create((set, get) => ({
34 | entries: [],
35 | financeReports: [],
36 | monthlyBalances: {} as Record,
37 | isCardUpdated: false,
38 | updatedCardId: "",
39 | selectedMonthYear: null,
40 |
41 |
42 |
43 | getEntries: () => {
44 | axios.get("/api/entries")
45 | .then((response) => {
46 | set({entries: response.data});
47 | }).catch(error)
48 | },
49 |
50 | getFinanceReports: () => {
51 | axios.get("/api/financeReports")
52 | .then((response) => {
53 | set({financeReports: response.data});
54 | }).catch(error)
55 | },
56 |
57 | getMonthlyBalances: () => {
58 | axios.get("/api/monthlybalance")
59 | .then((response) => {
60 | set({monthlyBalances: response.data});
61 | }).catch(error)
62 | },
63 |
64 | createEntry: (requestBody: EntryWithNoId) => {
65 | axios.post("/api/entries", requestBody)
66 | .catch(error)
67 | .then(() => {
68 | })
69 |
70 | },
71 |
72 | updateEntry: (requestBody: EntryWithNoId, id: string) => {
73 | axios.put(
74 | "/api/entries/" + id,
75 | requestBody
76 | ).catch(error)
77 | .finally(()=> {
78 | set({isCardUpdated: false})
79 | })
80 |
81 | },
82 |
83 | deleteEntry: (id: string) => {
84 |
85 | axios.delete("/api/entries/" + id)
86 | .catch(error)
87 | .then(() => {
88 | set({isCardUpdated: false})
89 | })
90 | },
91 |
92 | setIsCardUpdated: (updated: boolean) => {
93 | set({isCardUpdated: updated})
94 | },
95 |
96 | setUpdatedCardId: (id: string) => {
97 | set({updatedCardId: id})
98 | },
99 |
100 | getIsCardUpdated: () => {
101 | return get().isCardUpdated
102 | },
103 |
104 | getUpdatedCard: () => {
105 | const getEntries = get().getEntries
106 | getEntries()
107 | const id = get().updatedCardId
108 | const entries = get().entries
109 | return entries.find(entry => entry.id === id)
110 | },
111 |
112 | setSelectedMonthYear: (monthYear: string | null) => {
113 | set({selectedMonthYear: monthYear})
114 | }
115 |
116 |
117 | }));
118 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: "Deploy App"
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build-frontend:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 |
14 | - uses: actions/setup-node@v3
15 | with:
16 | node-version: '18'
17 |
18 | - name: Build Frontend
19 | working-directory: frontend
20 | run: |
21 | npm install
22 | npm run build
23 |
24 | - uses: actions/upload-artifact@v3
25 | with:
26 | name: frontend-build
27 | path: frontend/dist/
28 |
29 | build-backend:
30 | runs-on: ubuntu-latest
31 | needs: build-frontend
32 | steps:
33 | - uses: actions/checkout@v2
34 |
35 | - uses: actions/download-artifact@v2
36 | with:
37 | name: frontend-build
38 | path: backend/src/main/resources/static
39 |
40 | - name: Set up JDK
41 | uses: actions/setup-java@v2
42 | with:
43 | #Set Java Version
44 | java-version: '20'
45 | distribution: 'adopt'
46 | cache: 'maven'
47 |
48 | - name: Build with maven
49 | run: mvn -B package --file backend/pom.xml
50 |
51 | - uses: actions/upload-artifact@v2
52 | with:
53 | name: app.jar
54 | path: backend/target/budgetaryControl.jar
55 |
56 | push-to-docker-hub:
57 | runs-on: ubuntu-latest
58 | needs: build-backend
59 | steps:
60 | - uses: actions/checkout@v2
61 |
62 | - uses: actions/download-artifact@v2
63 | with:
64 | name: app.jar
65 | path: backend/target
66 |
67 | - name: Login to DockerHub
68 | uses: docker/login-action@v1
69 | with:
70 | #Set dockerhub username
71 | username: kajochin
72 | password: ${{ secrets.DOCKERHUB_PASSWORD }}
73 |
74 | - name: Build and push
75 | uses: docker/build-push-action@v2
76 | with:
77 | push: true
78 | #Set dockerhub project (replace "bartfastiel/java-capstone-project.de-example-app")
79 | tags: kajochin/capstone-budgetary-control:latest
80 | context: .
81 |
82 | deploy:
83 | runs-on: ubuntu-latest
84 | needs: push-to-docker-hub
85 | steps:
86 | - name: Restart docker container
87 | uses: appleboy/ssh-action@master
88 | with:
89 | host: capstone-project.de
90 | #Set App Name (replace "example" with your ssh user name)
91 | username: cgn-java-23-2-christoph
92 | password: ${{ secrets.SSH_PASSWORD }}
93 | #Set App Name (replace "example" with your ssh user name)
94 | #Set dockerhub project (replace "bartfastiel/java-capstone-project.de-example-app")
95 | #Set IP (replace "10.0.1.99" with your ip address)
96 | script: |
97 | sudo docker stop cgn-java-23-2-christoph
98 | sudo docker rm cgn-java-23-2-christoph
99 | sudo docker run --pull=always --name cgn-java-23-2-christoph --network capstones --ip 10.0.5.6 --restart always --detach --env MONGO_DB_URI=${{ secrets.MONGO_DB_URI }} kajochin/capstone-budgetary-control:latest
100 | sleep 15s
101 | sudo docker logs cgn-java-23-2-christoph
102 |
103 | - name: Check the deployed service URL
104 | uses: jtalk/url-health-check-action@v3
105 | with:
106 | #Set App Name (replace "example" with your ssh user name)
107 | url: http://cgn-java-23-2-christoph.capstone-project.de
108 | max-attempts: 3
109 | retry-delay: 5s
110 | retry-all: true
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/financereport/FinanceReportCalculate.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.financereport;
2 |
3 | import de.neuefische.capstone.backend.entries.EntriesRepo;
4 | import de.neuefische.capstone.backend.model.*;
5 | import lombok.AllArgsConstructor;
6 | import lombok.Data;
7 | import org.springframework.stereotype.Component;
8 |
9 | import java.math.BigDecimal;
10 | import java.math.RoundingMode;
11 | import java.util.ArrayList;
12 | import java.util.List;
13 |
14 | import static de.neuefische.capstone.backend.model.Interval.getMultiplier;
15 |
16 | @Data
17 | @AllArgsConstructor
18 | @Component
19 | public class FinanceReportCalculate {
20 |
21 | private final EntriesRepo entriesRepo;
22 |
23 |
24 | public List calculateFinanceReports() {
25 | List filteredEntries = new ArrayList<>();
26 | entriesRepo.findAll().stream().filter(entry -> !entry.getInterval().equals(Interval.ONCE)).forEach(filteredEntries::add);
27 |
28 | List financeReports = new ArrayList<>();
29 |
30 | financeReports.add(calculateAverageForPeriod(filteredEntries, Interval.MONTHLY));
31 | financeReports.add(calculateAverageForPeriod(filteredEntries, Interval.QUARTERLY));
32 | financeReports.add(calculateAverageForPeriod(filteredEntries, Interval.HALF_YEARLY));
33 | financeReports.add(calculateAverageForPeriod(filteredEntries, Interval.YEARLY));
34 |
35 | return financeReports;
36 | }
37 |
38 |
39 | private FinanceReport calculateAverageForPeriod(List filteredEntries, Interval period) {
40 |
41 |
42 | BigDecimal fixIncome = BigDecimal.ZERO;
43 | BigDecimal variableIncome = BigDecimal.ZERO;
44 | BigDecimal totalIncome;
45 | BigDecimal fixExpenses = BigDecimal.ZERO;
46 | BigDecimal variableExpenses = BigDecimal.ZERO;
47 | BigDecimal totalExpenses;
48 | BigDecimal fixCosts = BigDecimal.ZERO;
49 | BigDecimal totalVariableCosts = BigDecimal.ZERO;
50 | BigDecimal balance;
51 |
52 |
53 | for (Entry entry : filteredEntries) {
54 |
55 | BigDecimal intervalNum = calculateIntervalNum(entry.getInterval(), period);
56 |
57 | if (entry.getCostType().equals(CostType.FIXED)) {
58 | if (entry.getCategory().equals(Category.INCOME)) {
59 | fixIncome = fixIncome.add(entry.getAmount().multiply(intervalNum));
60 | } else {
61 | fixCosts = fixCosts.add(entry.getAmount().multiply(intervalNum));
62 | fixExpenses = fixCosts;
63 | }
64 | } else {
65 | if (entry.getCategory().equals(Category.INCOME)) {
66 | variableIncome = variableIncome.add(entry.getAmount().multiply(intervalNum));
67 | } else {
68 | totalVariableCosts = totalVariableCosts.add(entry.getAmount().multiply(intervalNum));
69 | variableExpenses = totalVariableCosts;
70 | }
71 |
72 | }
73 | }
74 |
75 |
76 | totalIncome = fixIncome.add(variableIncome);
77 | totalExpenses = fixExpenses.add(variableExpenses);
78 | balance = totalIncome.subtract(totalExpenses);
79 |
80 |
81 | return new FinanceReport(period, totalIncome, totalExpenses, fixCosts, totalVariableCosts, balance);
82 |
83 | }
84 |
85 | private BigDecimal calculateIntervalNum(Interval entryInterval, Interval reportPeriod) {
86 | int entryMultiplier = getMultiplier(entryInterval);
87 | int reportMultiplier = getMultiplier(reportPeriod);
88 |
89 | return BigDecimal.valueOf(reportMultiplier).divide(BigDecimal.valueOf(entryMultiplier), 3, RoundingMode.HALF_DOWN);
90 |
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/financereport/FinanceReportServiceTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.financereport;
2 |
3 | import de.neuefische.capstone.backend.model.FinanceReport;
4 | import de.neuefische.capstone.backend.model.Interval;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.math.BigDecimal;
8 | import java.util.List;
9 |
10 | import static org.junit.jupiter.api.Assertions.assertEquals;
11 | import static org.mockito.Mockito.mock;
12 | import static org.mockito.Mockito.when;
13 |
14 | class FinanceReportServiceTest {
15 |
16 | FinanceReportCalculate financeReportCalculate = mock(FinanceReportCalculate.class);
17 | FinanceReportService financeReportService = new FinanceReportService(financeReportCalculate);
18 |
19 | @Test
20 | void getFinanceReports() {
21 | //GIVEN
22 | List givenFinanceReports = List.of(
23 | new FinanceReport(
24 | Interval.MONTHLY,
25 | new BigDecimal("1000.000"),
26 | new BigDecimal("700.000"),
27 | new BigDecimal("500.000"),
28 | new BigDecimal("200.000"),
29 | new BigDecimal("300.000")
30 | ),
31 | new FinanceReport(
32 | Interval.QUARTERLY,
33 | new BigDecimal("1000.000"),
34 | new BigDecimal("700.000"),
35 | new BigDecimal("500.000"),
36 | new BigDecimal("200.000"),
37 | new BigDecimal("300.000")
38 | ),
39 | new FinanceReport(
40 | Interval.HALF_YEARLY,
41 | new BigDecimal("1000.000"),
42 | new BigDecimal("700.000"),
43 | new BigDecimal("500.000"),
44 | new BigDecimal("200.000"),
45 | new BigDecimal("300.000")
46 | ),
47 | new FinanceReport(
48 | Interval.YEARLY,
49 | new BigDecimal("1000.000"),
50 | new BigDecimal("700.000"),
51 | new BigDecimal("500.000"),
52 | new BigDecimal("200.000"),
53 | new BigDecimal("300.000")
54 | )
55 | );
56 | //WHEN
57 | when(financeReportCalculate.calculateFinanceReports()).thenReturn(givenFinanceReports);
58 | List expected = List.of(
59 | new FinanceReport(
60 | Interval.MONTHLY,
61 | new BigDecimal("1000.000"),
62 | new BigDecimal("700.000"),
63 | new BigDecimal("500.000"),
64 | new BigDecimal("200.000"),
65 | new BigDecimal("300.000")
66 | ),
67 | new FinanceReport(
68 | Interval.QUARTERLY,
69 | new BigDecimal("1000.000"),
70 | new BigDecimal("700.000"),
71 | new BigDecimal("500.000"),
72 | new BigDecimal("200.000"),
73 | new BigDecimal("300.000")
74 | ),
75 | new FinanceReport(
76 | Interval.HALF_YEARLY,
77 | new BigDecimal("1000.000"),
78 | new BigDecimal("700.000"),
79 | new BigDecimal("500.000"),
80 | new BigDecimal("200.000"),
81 | new BigDecimal("300.000")
82 | ),
83 | new FinanceReport(
84 | Interval.YEARLY,
85 | new BigDecimal("1000.000"),
86 | new BigDecimal("700.000"),
87 | new BigDecimal("500.000"),
88 | new BigDecimal("200.000"),
89 | new BigDecimal("300.000")
90 | )
91 | );
92 | List actual = financeReportService.getFinanceReports();
93 |
94 | //THEN
95 | assertEquals(expected, actual);
96 | }
97 |
98 | }
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/entries/EntriesServiceTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.entries;
2 |
3 | import de.neuefische.capstone.backend.model.*;
4 | import org.junit.jupiter.api.Test;
5 |
6 |
7 | import java.math.BigDecimal;
8 | import java.time.LocalDate;
9 | import java.util.List;
10 |
11 | import static org.junit.jupiter.api.Assertions.*;
12 | import static org.mockito.Mockito.*;
13 |
14 | class EntriesServiceTest {
15 |
16 | EntriesRepo entriesRepo = mock(EntriesRepo.class);
17 |
18 | IdService idService = mock(IdService.class);
19 |
20 | EntriesService entriesService = new EntriesService(entriesRepo, idService);
21 |
22 | @Test
23 | void ReturnEntriesWhenListIsNotEmpty() {
24 | //Given
25 | List entries = List.of(
26 | new Entry("1",
27 | "testTitle",
28 | "testDescription",
29 | LocalDate.of(2023, 12, 3),
30 | new BigDecimal(34),
31 | Category.INCOME,
32 | Interval.MONTHLY,
33 | CostType.FIXED)
34 | );
35 | //When
36 | when(entriesRepo.findAll()).thenReturn(entries);
37 | List actual = entriesService.getAllEntries();
38 | //Then
39 | verify(entriesRepo).findAll();
40 | assertEquals(entries, actual);
41 | }
42 |
43 | @Test
44 | void ReturnAddedEntryWhenEntryIsAdded() {
45 | //Given
46 | EntryWithNoId entryWithNoId = new EntryWithNoId(
47 | "testTitle",
48 | "testDescription",
49 | LocalDate.of(2023, 12, 3),
50 | new BigDecimal(34),
51 | Category.INCOME,
52 | Interval.MONTHLY,
53 | CostType.FIXED
54 | );
55 |
56 | String mockedID = "1";
57 | Entry entryExpected = new Entry(
58 | mockedID,
59 | "testTitle",
60 | "testDescription",
61 | LocalDate.of(2023, 12, 3),
62 | new BigDecimal(34),
63 | Category.INCOME,
64 | Interval.MONTHLY,
65 | CostType.FIXED);
66 | //When
67 | when(entriesRepo.insert(entryExpected)).thenReturn(entryExpected);
68 | when(idService.createRandomId()).thenReturn(mockedID);
69 | Entry actual = entriesService.addEntry(entryWithNoId);
70 | //Then
71 | verify(idService).createRandomId();
72 | verify(entriesRepo).insert(entryExpected);
73 | assertEquals(entryExpected, actual);
74 | }
75 |
76 | @Test
77 | void WhenEntryIsUpdatedReturnUpdatedEntry() {
78 | //Given
79 | Entry expectedEntry = new Entry(
80 | "1",
81 | "changedTitle",
82 | "changedDescription",
83 | LocalDate.of(2023, 12, 3),
84 | new BigDecimal(34),
85 | Category.INCOME,
86 | Interval.MONTHLY,
87 | CostType.FIXED
88 | );
89 |
90 | EntryWithNoId entryWithNoId = new EntryWithNoId(
91 | "changedTitle",
92 | "changedDescription",
93 | LocalDate.of(2023, 12, 3),
94 | new BigDecimal(34),
95 | Category.INCOME,
96 | Interval.MONTHLY,
97 | CostType.FIXED
98 | );
99 |
100 | String iD = "1";
101 | //When
102 | when(entriesRepo.save(expectedEntry)).thenReturn(expectedEntry);
103 | Entry actual = entriesService.updateEntry(entryWithNoId, iD);
104 | //Then
105 | verify(entriesRepo).save(expectedEntry);
106 | assertEquals(expectedEntry, actual);
107 | }
108 |
109 | @Test
110 | void WhenEntryIsDeletedReturnNothing() {
111 | //Given
112 | String iD = "1";
113 |
114 | //When
115 |
116 | when(entriesRepo.existsById(iD)).thenReturn(true);
117 |
118 | entriesService.deleteEntry(iD);
119 | //Then
120 | verify(entriesRepo).deleteById(iD);
121 | }
122 |
123 | @Test
124 | void WhenEntryIsDeletedAndDoesNotExistThrowException() {
125 | //Given
126 | String iD = "1";
127 | //When
128 | when(entriesRepo.existsById(iD)).thenReturn(false);
129 | //Then
130 | assertThrows(IllegalArgumentException.class, () -> entriesService.deleteEntry(iD));
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/frontend/src/monthlyBalance/MonthlyBalance.tsx:
--------------------------------------------------------------------------------
1 | import styled from "@emotion/styled";
2 | import {FormControl, InputLabel, MenuItem, Select} from "@mui/material";
3 | import {useEffect, useState} from "react";
4 | import EntriesList from "../entriesList/EntriesList.tsx";
5 | import {useStore} from "../hooks/useStore.ts";
6 | import AccountBalanceIcon from '@mui/icons-material/AccountBalance';
7 |
8 |
9 |
10 | export default function MonthlyBalance() {
11 |
12 | const selectedMonthYear = useStore((state) => state.selectedMonthYear)
13 |
14 | const initialYear = selectedMonthYear && selectedMonthYear !== "null" ? selectedMonthYear.split("-")[1] : "2023";
15 | const initialMonth = selectedMonthYear && selectedMonthYear !== "null" ? selectedMonthYear.split("-")[0] : "JANUARY";
16 |
17 | const [year, setYear] = useState(initialYear)
18 | const [month, setMonth] = useState(initialMonth)
19 |
20 | const monthYear = `${month}-${year}`
21 | const setSelectedMonthYear = useStore((state) => state.setSelectedMonthYear)
22 |
23 |
24 |
25 | useEffect(() => {
26 |
27 | setSelectedMonthYear(monthYear)
28 | }, [ setSelectedMonthYear, monthYear])
29 |
30 | return (
31 |
32 |
33 | Monthly Balance
34 |
35 |
36 |
37 |
38 | Here you can see your monthly balance.
39 | Select a month and year to view the balance and all transactions for that selected month.
40 |
41 |
42 |
43 |
44 | Choose a year
45 | setYear(e.target.value)}
48 | >
49 |
50 | 2023
51 | 2024
52 |
53 |
54 |
55 |
56 | Choose a month
57 | setMonth(e.target.value)}
60 | >
61 | January
62 | February
63 | March
64 | April
65 | May
66 | June
67 | July
68 | August
69 | September
70 | October
71 | November
72 | December
73 |
74 |
75 |
76 |
77 |
78 |
79 | )
80 | }
81 |
82 | const StyledH2 = styled.h2`
83 | font-family: "Roboto Light", sans-serif;
84 | display: flex;
85 | justify-content: center;
86 | margin-right: 4px;
87 | margin-top: 25px;
88 | `;
89 |
90 | const StyledDropDownContainer = styled.div`
91 | display: flex;
92 | flex-direction: column;
93 | justify-content: center;
94 | gap: 20px;
95 | margin: 25px;
96 | margin-top: 40px;
97 |
98 | `;
99 |
100 | const ExplonationText = styled.div`
101 | padding: 4px;
102 | border-radius: 7px;
103 | background-color: #edf0fc;
104 | font-family: "Roboto Light", sans-serif;
105 | display: flex;
106 | flex-direction: column;
107 | justify-content: center;
108 | margin-right: 20px;
109 | margin-left: 20px;
110 | margin-bottom: 25px;
111 | font-size: 16px;
112 | font-weight: 500;
113 | line-height: 1.6;
114 | letter-spacing: 0.0075em;
115 | text-align: center;
116 | box-shadow: 10px 10px 5px -4px silver;
117 | `;
118 |
119 | const HeadingDiv = styled.div`
120 | display: flex;
121 | justify-content: center;
122 | align-items: center;
123 | `;
124 |
--------------------------------------------------------------------------------
/backend/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 3.1.2
9 |
10 |
11 | de.neuefische.capstone
12 | backend
13 | 0.0.1-SNAPSHOT
14 | backend
15 | backend
16 |
17 | 20
18 | kajochi
19 | https://sonarcloud.io
20 | capstone-budgetary-control-backend
21 |
22 |
23 |
24 | org.powermock
25 | powermock-module-junit4
26 | 2.0.9
27 | test
28 |
29 |
30 |
31 | org.springframework.boot
32 | spring-boot-starter-web
33 |
34 |
35 | org.yaml
36 | snakeyaml
37 | 2.0
38 |
39 |
40 |
41 | org.projectlombok
42 | lombok
43 | true
44 |
45 |
46 | org.springframework.boot
47 | spring-boot-starter-test
48 | test
49 |
50 |
51 | org.springframework.boot
52 | spring-boot-starter-data-mongodb
53 | 3.1.1
54 |
55 |
56 |
57 | de.flapdoodle.embed
58 | de.flapdoodle.embed.mongo.spring30x
59 | test
60 | 4.6.2
61 |
62 |
63 | org.assertj
64 | assertj-core
65 | 3.4.1
66 | test
67 |
68 |
69 |
70 |
71 |
72 | budgetaryControl
73 |
74 |
75 | org.apache.maven.plugins
76 | maven-surefire-plugin
77 | 3.0.0-M5
78 |
79 |
80 | org.springframework.boot
81 | spring-boot-maven-plugin
82 |
83 |
84 |
85 | org.projectlombok
86 | lombok
87 |
88 |
89 |
90 |
91 |
92 | org.jacoco
93 | jacoco-maven-plugin
94 | 0.8.10
95 |
96 |
97 | jacoco-initialize
98 |
99 | prepare-agent
100 |
101 |
102 |
103 | jacoco-site
104 | package
105 |
106 | report
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/frontend/src/home/Home.tsx:
--------------------------------------------------------------------------------
1 |
2 | import styled from "@emotion/styled";
3 | import AddIcon from "@mui/icons-material/Add";
4 | import {Link, useSearchParams} from "react-router-dom";
5 | import {IconButton} from "@mui/material";
6 | import {useStore} from "../hooks/useStore.ts";
7 | import {useEffect} from "react";
8 | import HomeIcon from '@mui/icons-material/Home';
9 |
10 |
11 | export default function Home() {
12 |
13 | const [selectedMonthYear] = useSearchParams()
14 | const monthYear = selectedMonthYear.get("monthYear")
15 | const getEntries = useStore((state) => state.getEntries)
16 |
17 | useEffect(() => {
18 | getEntries()
19 | }, [])
20 |
21 | const entries = useStore((state) => state.entries)
22 |
23 | function existsEntries() : boolean {
24 | return entries.length > 0;
25 | }
26 |
27 | return (
28 |
29 |
30 | Home
31 |
32 |
33 |
34 | Welcome to Budgetary Control!
35 | This is a simple app that helps you keep track of your expenses and
36 | incomes.
37 | You can add entries, view your monthly balance and generate a finance report.
38 |
39 |
40 | How to use this app?
41 | {existsEntries() ? (
42 | You have already created transactions.
43 | Click on the burger menu at the top right to display the balance sheet for a specific month, for example.
44 | ) : (
45 | You have not yet created any transactions.
46 | Click on the plus symbol below or in the burger menu on the top right on Add entry to create transactions.
47 | )}
48 |
49 |
50 | Upcoming Features
51 |
52 | Sign up
53 | Log In / Log Out
54 | Excel table export
55 | Ideas for saving money
56 | Import your bills
57 | Dark Mode
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | )
67 | }
68 |
69 |
70 | const StyledH2 = styled.h2`
71 | font-family: "Roboto Light", sans-serif;
72 | display: flex;
73 | justify-content: center;
74 | margin-right: 4px;
75 | margin-top: 25px;
76 | `;
77 |
78 | const StyledLink = styled(Link)`
79 | display: flex;
80 | justify-content: center;
81 | `;
82 |
83 | const StyledIconButton = styled(IconButton)`
84 | background-color: #4d6bdd;
85 | display: flex;
86 | justify-content: center;
87 |
88 | &:hover {
89 | background-color: #129cfc;
90 | transition: all 0.3s;
91 | transform: scale(1.2);
92 | }
93 | `;
94 |
95 | const IntroductoryText = styled.div`
96 | padding: 16px;
97 | border-radius: 7px;
98 | background-color:#edf0fc;
99 | font-family: "Roboto Light", sans-serif;
100 | display: flex;
101 | flex-direction: column;
102 | justify-content: center;
103 | margin-right: 20px;
104 | margin-left: 20px;
105 | margin-bottom: 25px;
106 | font-size: 16px;
107 | font-weight: 500;
108 | line-height: 1.6;
109 | letter-spacing: 0.0075em;
110 | text-align: center;
111 | box-shadow: 10px 10px 5px -4px silver;
112 | `;
113 |
114 | const ExplanationText = styled.div`
115 | padding: 16px;
116 | border-radius: 7px;
117 | color: #ffffff;
118 | background-color: #4d6bdd;
119 | font-family: "Roboto Light", sans-serif;
120 | display: flex;
121 | flex-direction: column;
122 | justify-content: center;
123 | margin-right: 20px;
124 | margin-left: 20px;
125 | margin-bottom: 25px;
126 | font-size: 16px;
127 | font-weight: 500;
128 | line-height: 1.6;
129 | letter-spacing: 0.0075em;
130 | text-align: center;
131 | box-shadow: 10px 10px 5px -4px silver;
132 | `;
133 |
134 | const UpcomingFeatures = styled.div`
135 | padding: 16px;
136 | border-radius: 7px;
137 | background-color: #edf0fc;
138 | font-family: "Roboto Light", sans-serif;
139 | display: flex;
140 | flex-direction: column;
141 | justify-content: center;
142 | margin-right: 20px;
143 | margin-left: 20px;
144 | margin-bottom: 25px;
145 | font-size: 16px;
146 | font-weight: 500;
147 | line-height: 1.6;
148 | letter-spacing: 0.0075em;
149 | text-align: center;
150 | box-shadow: 10px 10px 5px -4px silver;
151 | `;
152 |
153 | const HeadingDiv = styled.div`
154 | display: flex;
155 | justify-content: center;
156 | align-items: center;
157 | `;
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/monthlybalance/MonthlySort.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.monthlybalance;
2 |
3 | import de.neuefische.capstone.backend.entries.EntriesRepo;
4 | import de.neuefische.capstone.backend.model.Entry;
5 | import de.neuefische.capstone.backend.model.Interval;
6 | import de.neuefische.capstone.backend.model.MonthlyBalance;
7 | import lombok.Data;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.stereotype.Component;
10 |
11 | import java.math.BigDecimal;
12 | import java.time.LocalDate;
13 | import java.util.ArrayList;
14 | import java.util.HashMap;
15 | import java.util.List;
16 | import java.util.Map;
17 |
18 | @Component
19 | @Data
20 | public class MonthlySort {
21 | @Autowired
22 | private final EntriesRepo entriesRepo;
23 |
24 | private List generateEarliestAndLatestMonthForMonthlyBalance() {
25 | LocalDate currentDate = LocalDate.now();
26 | LocalDate endDate = currentDate.plusMonths(12);
27 |
28 | Entry earliestEntry = entriesRepo.findFirstByOrderByDateAsc();
29 | LocalDate earliestTransactionDate = earliestEntry.getDate();
30 | LocalDate startDate = earliestTransactionDate.isBefore(currentDate) ? earliestTransactionDate : currentDate;
31 |
32 | return List.of(startDate, endDate);
33 | }
34 |
35 | public Map generateMonthlyBalanceList() {
36 | List dateList = generateEarliestAndLatestMonthForMonthlyBalance();
37 | LocalDate startDate = dateList.get(0);
38 | LocalDate endDate = dateList.get(1);
39 |
40 | Map monthlyBalanceList = new HashMap<>();
41 |
42 | while (startDate.isBefore(endDate)) {
43 | String monthYear = startDate.getMonth().toString() + "-" + startDate.getYear();
44 |
45 | List entriesSortedByStartDate = sortEntriesByStartDate(startDate);
46 | List entryAmounts = MonthlyBalanceCalculate.calculateEntryAmounts(entriesSortedByStartDate);
47 |
48 | MonthlyBalance monthlyBalance = new MonthlyBalance(monthYear,
49 | entryAmounts.get(0),
50 | entryAmounts.get(1),
51 | entryAmounts.get(2),
52 | entryAmounts.get(3),
53 | entryAmounts.get(4),
54 | entryAmounts.get(5),
55 | entriesSortedByStartDate);
56 |
57 | monthlyBalanceList.put(monthYear, monthlyBalance);
58 | startDate = startDate.plusMonths(1);
59 | }
60 | return monthlyBalanceList;
61 | }
62 |
63 | private List sortEntriesByStartDate(LocalDate targetDate) {
64 | List monthlyEntries = entriesRepo.findAll().stream()
65 | .filter(entry -> entry.getInterval() == Interval.MONTHLY)
66 | .filter(entry -> entry.getDate().isBefore(targetDate)
67 | || entry.getDate().getMonth() == targetDate.getMonth()
68 | && entry.getDate().getYear() == targetDate.getYear())
69 | .toList();
70 |
71 | List onceEntries = entriesRepo.findAll().stream()
72 | .filter(entry -> entry.getInterval() == Interval.ONCE)
73 | .filter(entry -> entry.getDate().getMonth() == targetDate.getMonth())
74 | .filter(entry -> entry.getDate().getYear() == targetDate.getYear())
75 | .toList();
76 |
77 | List recurringEntries = entriesRepo.findAll().stream()
78 | .filter(entry -> entry.getInterval() == Interval.QUARTERLY
79 | || entry.getInterval() == Interval.HALF_YEARLY
80 | || entry.getInterval() == Interval.YEARLY)
81 | .filter(entry -> entry.getDate().isBefore(targetDate)
82 | || entry.getDate().getMonth() == targetDate.getMonth()
83 | && entry.getDate().getYear() == targetDate.getYear())
84 | .filter(entry -> {
85 | int multiplier = Interval.getMultiplier(entry.getInterval());
86 | return calculateApplicableMonths(multiplier, entry.getDate().getMonthValue(), targetDate.getMonthValue());
87 | })
88 | .toList();
89 |
90 | List sortedEntries = new ArrayList<>();
91 | sortedEntries.addAll(monthlyEntries);
92 | sortedEntries.addAll(recurringEntries);
93 | sortedEntries.addAll(onceEntries);
94 |
95 | return sortedEntries;
96 |
97 | }
98 |
99 | private boolean calculateApplicableMonths(int interval, int startMonth, int targetMonth) {
100 | List applicableMonths = new ArrayList<>();
101 | applicableMonths.add(startMonth);
102 | for(int i = 0; i <= interval; i++){
103 | startMonth = startMonth + interval;
104 | if (startMonth > 12){
105 | applicableMonths.add(startMonth - 12);
106 |
107 | }else {
108 | applicableMonths.add(startMonth);
109 | }
110 | }
111 |
112 | return applicableMonths.contains(targetMonth);
113 |
114 | }
115 |
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/frontend/src/financeReportCard/FinanceReportCard.tsx:
--------------------------------------------------------------------------------
1 |
2 | import {FinanceReport} from "../model/FinanceReport.ts";
3 | import styled from "@emotion/styled";
4 | import {Interval} from "../model/Interval.ts";
5 | import Divider from "@mui/material/Divider";
6 |
7 | type Props = {
8 | period: Interval,
9 | financeReports: FinanceReport[]
10 | }
11 |
12 | export default function FinanceReportCard(props: Props){
13 |
14 | let financeReport: FinanceReport
15 |
16 | if (props.period === 'MONTHLY') {
17 | financeReport = props.financeReports[0]
18 | } else if (props.period === 'QUARTERLY') {
19 | financeReport = props.financeReports[1]
20 | } else if (props.period === 'HALF_YEARLY') {
21 | financeReport = props.financeReports[2]
22 | } else {
23 | financeReport = props.financeReports[3]
24 | }
25 |
26 | if (financeReport === undefined) {
27 | return loading...
28 | }
29 | return (
30 |
31 |
32 | Total income: {financeReport.totalIncome}€
33 |
34 | Total expense: {financeReport.totalExpenses}€
35 |
36 | Fix costs: {financeReport.fixCosts}€
37 |
38 | Variable costs: {financeReport.variableCosts}€
39 |
40 |
41 |
42 |
43 | Balance: {financeReport.balance}€
44 |
45 |
46 | )
47 | }
48 | const StyledDivContainer = styled.div`
49 | display: grid;
50 | border: 1px solid #c2bfbf;
51 | border-radius: 7px;
52 | padding: 10px;
53 | grid-template-columns: 3fr 1fr;
54 | grid-template-rows: repeat(10, auto);
55 | justify-content: space-around;
56 | margin-left: 30px;
57 | margin-right: 30px;
58 | box-shadow: 10px 10px 5px -4px silver;
59 | grid-template-areas:
60 | "totalIncome totalIncomeAmount"
61 | "divider1 divider1"
62 | "totalExpense totalExpenseAmount"
63 | "divider2 divider2"
64 | "fixCosts fixCostsAmount"
65 | "divider3 divider3"
66 | "variableCosts variableCostsAmount"
67 | "dividerDiv dividerDiv"
68 | "balance balanceAmount"
69 |
70 | `;
71 |
72 | const TotalIncomeP = styled.p`
73 | grid-area: totalIncome;
74 | font-family: "Roboto Light", sans-serif;
75 | `;
76 |
77 | const TotalIncomeAmountP = styled.p`
78 | grid-area: totalIncomeAmount;
79 | font-family: "Roboto Light", sans-serif;
80 | display: flex;
81 | justify-content: flex-end;
82 | `;
83 | const TotalExpenseP = styled.p`
84 | grid-area: totalExpense;
85 | font-family: "Roboto Light", sans-serif;
86 | `;
87 | const TotalExpenseAmountP = styled.p`
88 | grid-area: totalExpenseAmount;
89 | font-family: "Roboto Light", sans-serif;
90 | display: flex;
91 | justify-content: flex-end;
92 | `;
93 | const FixCostsP = styled.p`
94 | grid-area: fixCosts;
95 | font-family: "Roboto Light", sans-serif;
96 | `;
97 | const FixCostsAmountP = styled.p`
98 | grid-area: fixCostsAmount;
99 | font-family: "Roboto Light", sans-serif;
100 | display: flex;
101 | justify-content: flex-end;
102 | `;
103 | const VariableCostsP = styled.p`
104 | grid-area: variableCosts;
105 | font-family: "Roboto Light", sans-serif;
106 | `;
107 | const VariableCostsAmountP = styled.p`
108 | grid-area: variableCostsAmount;
109 | font-family: "Roboto Light", sans-serif;
110 | display: flex;
111 | justify-content: flex-end;
112 | `;
113 |
114 | const BalanceP = styled.p`
115 | grid-area: balance;
116 | font-family: "Roboto Light", sans-serif;
117 | font-weight: bold;
118 | `;
119 | const BalanceAmountP = styled.p`
120 | grid-area: balanceAmount;
121 | font-family: "Roboto Light", sans-serif;
122 | display: flex;
123 | justify-content: flex-end;
124 | font-weight: bold;
125 | `;
126 | const Divider1 = styled(Divider)`
127 | grid-area: divider1;
128 | font-family: "Roboto Light", sans-serif;
129 | display: flex;
130 | justify-content: flex-end;
131 | `;
132 | const Divider2 = styled(Divider)`
133 | grid-area: divider2;
134 | font-family: "Roboto Light", sans-serif;
135 | display: flex;
136 | justify-content: flex-end;
137 | `;
138 | const Divider3 = styled(Divider)`
139 | grid-area: divider3;
140 | font-family: "Roboto Light", sans-serif;
141 | display: flex;
142 | justify-content: flex-end;
143 | `;
144 | const Divider4 = styled(Divider)`
145 | grid-area: divider4;
146 | font-family: "Roboto Light", sans-serif;
147 | display: flex;
148 | justify-content: flex-end;
149 | `;
150 | const Divider5 = styled(Divider)`
151 | grid-area: divider5;
152 | font-family: "Roboto Light", sans-serif;
153 | display: flex;
154 | justify-content: flex-end;
155 | `;
156 |
157 |
158 | const DividerDiv = styled.div`
159 | grid-area: dividerDiv;
160 |
161 | `;
--------------------------------------------------------------------------------
/frontend/src/monthlyBalanceAmountsCard/MonthlyBalanceAmountsCard.tsx:
--------------------------------------------------------------------------------
1 | import {MonthlyBalance} from "../model/MonthlyBalance.ts";
2 | import styled from "@emotion/styled";
3 | import Divider from '@mui/material/Divider';
4 |
5 |
6 | type Props = {
7 | monthlyBalance: MonthlyBalance;
8 | }
9 | export default function MonthlyBalanceAmountsCard(props: Props) {
10 |
11 |
12 |
13 | return (
14 | <>
15 | {props.monthlyBalance? (
16 | Total income: {props.monthlyBalance.totalIncome}€
17 |
18 | Total expense: {props.monthlyBalance.totalExpenses}€
19 |
20 | Fix costs: {props.monthlyBalance.fixedCosts}€
21 |
22 | Variable costs: {props.monthlyBalance.variableCosts}€
23 |
24 | One time costs: {props.monthlyBalance.oneTimeCosts}€
25 |
26 |
27 |
28 |
29 | Balance: {props.monthlyBalance.balance}€
30 |
31 | ) : (
32 | loading...
33 | )}
34 | >
35 | )
36 | }
37 |
38 | const StyledDivContainer = styled.div`
39 | display: grid;
40 | border: 1px solid #c2bfbf;
41 | border-radius: 7px;
42 | padding: 10px;
43 | grid-template-columns: 3fr 1fr;
44 | grid-template-rows: repeat(10, auto);
45 | justify-content: space-around;
46 | margin-left: 30px;
47 | margin-right: 30px;
48 | box-shadow: 10px 10px 5px -4px silver;
49 | grid-template-areas:
50 | "totalIncome totalIncomeAmount"
51 | "divider1 divider1"
52 | "totalExpense totalExpenseAmount"
53 | "divider2 divider2"
54 | "fixCosts fixCostsAmount"
55 | "divider3 divider3"
56 | "variableCosts variableCostsAmount"
57 | "divider4 divider4"
58 | "oneTimeCosts oneTimeCostsAmount"
59 | "dividerDiv dividerDiv"
60 | "balance balanceAmount"
61 |
62 | `;
63 |
64 | const TotalIncomeP = styled.p`
65 | grid-area: totalIncome;
66 | font-family: "Roboto Light", sans-serif;
67 | `;
68 |
69 | const TotalIncomeAmountP = styled.p`
70 | grid-area: totalIncomeAmount;
71 | font-family: "Roboto Light", sans-serif;
72 | display: flex;
73 | justify-content: flex-end;
74 | `;
75 | const TotalExpenseP = styled.p`
76 | grid-area: totalExpense;
77 | font-family: "Roboto Light", sans-serif;
78 | `;
79 | const TotalExpenseAmountP = styled.p`
80 | grid-area: totalExpenseAmount;
81 | font-family: "Roboto Light", sans-serif;
82 | display: flex;
83 | justify-content: flex-end;
84 | `;
85 | const FixCostsP = styled.p`
86 | grid-area: fixCosts;
87 | font-family: "Roboto Light", sans-serif;
88 | `;
89 | const FixCostsAmountP = styled.p`
90 | grid-area: fixCostsAmount;
91 | font-family: "Roboto Light", sans-serif;
92 | display: flex;
93 | justify-content: flex-end;
94 | `;
95 | const VariableCostsP = styled.p`
96 | grid-area: variableCosts;
97 | font-family: "Roboto Light", sans-serif;
98 | `;
99 | const VariableCostsAmountP = styled.p`
100 | grid-area: variableCostsAmount;
101 | font-family: "Roboto Light", sans-serif;
102 | display: flex;
103 | justify-content: flex-end;
104 | `;
105 | const OneTimeCostsP = styled.p`
106 | grid-area: oneTimeCosts;
107 | font-family: "Roboto Light", sans-serif;
108 | `;
109 | const OneTimeCostsAmountP = styled.p`
110 | grid-area: oneTimeCostsAmount;
111 | font-family: "Roboto Light", sans-serif;
112 | display: flex;
113 | justify-content: flex-end;
114 | `;
115 | const BalanceP = styled.p`
116 | grid-area: balance;
117 | font-family: "Roboto Light", sans-serif;
118 | font-weight: bold;
119 | `;
120 | const BalanceAmountP = styled.p`
121 | grid-area: balanceAmount;
122 | font-family: "Roboto Light", sans-serif;
123 | display: flex;
124 | justify-content: flex-end;
125 | font-weight: bold;
126 | `;
127 | const Divider1 = styled(Divider)`
128 | grid-area: divider1;
129 | font-family: "Roboto Light", sans-serif;
130 | display: flex;
131 | justify-content: flex-end;
132 | `;
133 | const Divider2 = styled(Divider)`
134 | grid-area: divider2;
135 | font-family: "Roboto Light", sans-serif;
136 | display: flex;
137 | justify-content: flex-end;
138 | `;
139 | const Divider3 = styled(Divider)`
140 | grid-area: divider3;
141 | font-family: "Roboto Light", sans-serif;
142 | display: flex;
143 | justify-content: flex-end;
144 | `;
145 | const Divider4 = styled(Divider)`
146 | grid-area: divider4;
147 | font-family: "Roboto Light", sans-serif;
148 | display: flex;
149 | justify-content: flex-end;
150 | `;
151 | const Divider5 = styled(Divider)`
152 | grid-area: divider5;
153 | font-family: "Roboto Light", sans-serif;
154 | display: flex;
155 | justify-content: flex-end;
156 | `;
157 | const Divider6 = styled(Divider)`
158 | grid-area: divider6;
159 | font-family: "Roboto Light", sans-serif;
160 | display: flex;
161 | justify-content: flex-end;
162 | `;
163 |
164 | const DividerDiv = styled.div`
165 | grid-area: dividerDiv;
166 |
167 | `;
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/financereport/FinanceReportIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.financereport;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import de.neuefische.capstone.backend.model.*;
5 | import org.junit.jupiter.api.Test;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
8 | import org.springframework.boot.test.context.SpringBootTest;
9 | import org.springframework.http.MediaType;
10 | import org.springframework.test.annotation.DirtiesContext;
11 | import org.springframework.test.web.servlet.MockMvc;
12 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
13 |
14 | import java.math.BigDecimal;
15 | import java.time.LocalDate;
16 | import java.util.List;
17 |
18 | import static org.junit.jupiter.api.Assertions.assertEquals;
19 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
20 |
21 | @SpringBootTest
22 | @AutoConfigureMockMvc
23 | class FinanceReportIntegrationTest {
24 | @Autowired
25 | MockMvc mockMvc;
26 | @Autowired
27 | FinanceReportService financeReportService;
28 | @Autowired
29 | ObjectMapper objectMapper;
30 |
31 | @DirtiesContext
32 | @Test
33 | void WhenEntriesAreAddedReturnListOfFinanceReports() throws Exception {
34 | //Given
35 | String jsonRequestBodyFirstEntry = objectMapper.writeValueAsString(new EntryWithNoId(
36 | "testTitle",
37 | "testDescription",
38 | LocalDate.of(2023, 12, 3),
39 | new BigDecimal(1000),
40 | Category.INCOME,
41 | Interval.MONTHLY,
42 | CostType.FIXED
43 | ));
44 | String jsonRequestBodySecondEntry = objectMapper.writeValueAsString(new EntryWithNoId(
45 | "testTitle",
46 | "testDescription",
47 | LocalDate.of(2023, 12, 3),
48 | new BigDecimal(500),
49 | Category.EXPENSE,
50 | Interval.MONTHLY,
51 | CostType.FIXED
52 | ));
53 | String jsonRequestBodyThirdEntry = objectMapper.writeValueAsString(new EntryWithNoId(
54 | "testTitle",
55 | "testDescription",
56 | LocalDate.of(2023, 12, 3),
57 | new BigDecimal(200),
58 | Category.EXPENSE,
59 | Interval.MONTHLY,
60 | CostType.VARIABLE
61 | ));
62 | //When
63 | mockMvc.perform(
64 | MockMvcRequestBuilders.post("/api/entries")
65 | .contentType(MediaType.APPLICATION_JSON)
66 | .content(jsonRequestBodyFirstEntry)
67 | )
68 | .andExpect(status().isOk());
69 | mockMvc.perform(
70 | MockMvcRequestBuilders.post("/api/entries")
71 | .contentType(MediaType.APPLICATION_JSON)
72 | .content(jsonRequestBodySecondEntry)
73 | )
74 | .andExpect(status().isOk());
75 | mockMvc.perform(
76 | MockMvcRequestBuilders.post("/api/entries")
77 | .contentType(MediaType.APPLICATION_JSON)
78 | .content(jsonRequestBodyThirdEntry)
79 | )
80 | .andExpect(status().isOk());
81 |
82 | String responseString = mockMvc.perform(
83 | MockMvcRequestBuilders.get("/api/financeReports")
84 | )
85 |
86 | //THEN
87 | .andExpect(status().isOk())
88 | .andExpect(content().contentType(MediaType.APPLICATION_JSON))
89 | .andReturn()
90 | .getResponse()
91 | .getContentAsString();
92 |
93 | List actual = objectMapper.readValue(responseString, new com.fasterxml.jackson.core.type.TypeReference<>() {
94 | });
95 | List expected = List.of(
96 | new FinanceReport(
97 | Interval.MONTHLY,
98 | new BigDecimal("1000.000"),
99 | new BigDecimal("700.000"),
100 | new BigDecimal("500.000"),
101 | new BigDecimal("200.000"),
102 | new BigDecimal("300.000")
103 | ),
104 | new FinanceReport(
105 | Interval.QUARTERLY,
106 | new BigDecimal("3000.000"),
107 | new BigDecimal("2100.000"),
108 | new BigDecimal("1500.000"),
109 | new BigDecimal("600.000"),
110 | new BigDecimal("900.000")
111 | ),
112 | new FinanceReport(
113 | Interval.HALF_YEARLY,
114 | new BigDecimal("6000.000"),
115 | new BigDecimal("4200.000"),
116 | new BigDecimal("3000.000"),
117 | new BigDecimal("1200.000"),
118 | new BigDecimal("1800.000")
119 | ),
120 | new FinanceReport(
121 | Interval.YEARLY,
122 | new BigDecimal("12000.000"),
123 | new BigDecimal("8400.000"),
124 | new BigDecimal("6000.000"),
125 | new BigDecimal("2400.000"),
126 | new BigDecimal("3600.000")
127 | ));
128 | assertEquals(expected, actual);
129 | }
130 |
131 | }
132 |
133 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/entries/EntriesIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.entries;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import de.neuefische.capstone.backend.model.*;
5 | import org.junit.jupiter.api.Test;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
8 | import org.springframework.boot.test.context.SpringBootTest;
9 | import org.springframework.http.MediaType;
10 | import org.springframework.test.annotation.DirtiesContext;
11 | import org.springframework.test.web.servlet.MockMvc;
12 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
13 |
14 | import java.math.BigDecimal;
15 | import java.time.LocalDate;
16 |
17 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
18 |
19 | @SpringBootTest
20 | @AutoConfigureMockMvc
21 | class EntriesIntegrationTest {
22 | @Autowired
23 | MockMvc mockMvc;
24 | @Autowired
25 | EntriesService entriesService;
26 | @Autowired
27 | ObjectMapper objectMapper;
28 |
29 | @DirtiesContext
30 | @Test
31 | void WhenListIsEmptyReturnEmptyList() throws Exception {
32 | //When
33 | mockMvc.perform(
34 | MockMvcRequestBuilders.get("/api/entries")
35 |
36 |
37 | )
38 | //Then
39 | .andExpect(content().contentType(MediaType.APPLICATION_JSON))
40 | .andExpect(jsonPath("$").isEmpty())
41 | .andExpect(status().isOk());
42 | }
43 |
44 | @DirtiesContext
45 | @Test
46 | void WhenEntryIsAddedReturnAddedEntry() throws Exception {
47 | //Given
48 | String jsonRequestBody = objectMapper.writeValueAsString(new EntryWithNoId(
49 | "testTitle",
50 | "testDescription",
51 | LocalDate.of(2023, 12, 3),
52 | new BigDecimal(34),
53 | Category.INCOME,
54 | Interval.MONTHLY,
55 | CostType.FIXED
56 | ));
57 | //When
58 | mockMvc.perform(
59 | MockMvcRequestBuilders.post("/api/entries")
60 | .contentType(MediaType.APPLICATION_JSON)
61 | .content(jsonRequestBody)
62 | )
63 | //Then
64 | .andExpect(content().contentType(MediaType.APPLICATION_JSON))
65 | .andExpect(jsonPath("title").value("testTitle"))
66 | .andExpect(jsonPath("description").value("testDescription"))
67 | .andExpect(jsonPath("date").value("2023-12-03"))
68 | .andExpect(jsonPath("amount").value("34"))
69 | .andExpect(jsonPath("category").value("INCOME"))
70 | .andExpect(jsonPath("interval").value("MONTHLY"))
71 | .andExpect(jsonPath("costType").value("FIXED"))
72 | .andExpect(status().isOk());
73 | }
74 |
75 | @DirtiesContext
76 | @Test
77 | void WhenEntryIsUpdatedReturnUpdatedEntry() throws Exception {
78 | //Given
79 | String jsonRequestBody = objectMapper.writeValueAsString(new EntryWithNoId(
80 | "changedTitle",
81 | "changedDescription",
82 | LocalDate.of(2023, 12, 3),
83 | new BigDecimal(34),
84 | Category.INCOME,
85 | Interval.MONTHLY,
86 | CostType.FIXED
87 | ));
88 | String id = "1";
89 | //When
90 | mockMvc.perform(
91 | MockMvcRequestBuilders.put("/api/entries/" + id)
92 | .contentType(MediaType.APPLICATION_JSON)
93 | .content(jsonRequestBody)
94 | )
95 | //Then
96 | .andExpect(content().contentType(MediaType.APPLICATION_JSON))
97 | .andExpect(jsonPath("title").value("changedTitle"))
98 | .andExpect(jsonPath("description").value("changedDescription"))
99 | .andExpect(jsonPath("date").value("2023-12-03"))
100 | .andExpect(jsonPath("amount").value("34"))
101 | .andExpect(jsonPath("category").value("INCOME"))
102 | .andExpect(jsonPath("interval").value("MONTHLY"))
103 | .andExpect(jsonPath("costType").value("FIXED"))
104 | .andExpect(status().isOk());
105 | }
106 |
107 | @DirtiesContext
108 | @Test
109 | void WhenDeleteEntryReturnEmptyList() throws Exception {
110 | //Given
111 | String jsonRequestBody = objectMapper.writeValueAsString(new EntryWithNoId(
112 | "testTitle",
113 | "testDescription",
114 | LocalDate.of(2023, 12, 3),
115 | new BigDecimal(34),
116 | Category.INCOME,
117 | Interval.MONTHLY,
118 | CostType.FIXED
119 | ));
120 |
121 | String responseString = mockMvc.perform(
122 | MockMvcRequestBuilders.post("/api/entries")
123 | .contentType(MediaType.APPLICATION_JSON)
124 | .content(jsonRequestBody)
125 | )
126 | .andReturn()
127 | .getResponse()
128 | .getContentAsString();
129 |
130 | Entry entry = objectMapper.readValue(responseString, Entry.class);
131 | //When
132 | mockMvc.perform(
133 | MockMvcRequestBuilders.delete("/api/entries/" + entry.getId())
134 |
135 | );
136 | //Then
137 | mockMvc.perform(
138 | MockMvcRequestBuilders.get("/api/entries")
139 | )
140 | //Then
141 | .andExpect(status().isOk())
142 | .andExpect(content().contentType(MediaType.APPLICATION_JSON))
143 | .andExpect(content().json("[]"));
144 |
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/frontend/src/entryCard/EntryCard.tsx:
--------------------------------------------------------------------------------
1 | import {Entry} from "../model/Entry.ts";
2 | import Card from '@mui/material/Card';
3 | import CardContent from '@mui/material/CardContent';
4 | import Typography from '@mui/material/Typography';
5 | import styled from "@emotion/styled";
6 | import {useNavigate} from "react-router-dom";
7 | import {useStore} from "../hooks/useStore.ts";
8 | import {IconButton} from "@mui/material";
9 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
10 | import ExpandLessIcon from '@mui/icons-material/ExpandLess';
11 | import {useState} from "react";
12 | import {Category} from "../model/Category.ts";
13 | import EditIcon from '@mui/icons-material/Edit';
14 |
15 |
16 | type Props = {
17 | entry: Entry;
18 | monthYear: string;
19 | }
20 | export default function EntryCard(props: Props) {
21 |
22 | const [expanded, setExpanded] = useState(false);
23 | const navigate = useNavigate()
24 |
25 | const setIsCardUpdated = useStore((state) => state.setIsCardUpdated)
26 | const setUpdatedCardId = useStore((state) => state.setUpdatedCardId)
27 |
28 | function handleClick() {
29 | navigate(`/add-entry`)
30 | setIsCardUpdated(true)
31 | setUpdatedCardId(props.entry.id)
32 | }
33 |
34 | function toggleExpanded() {
35 | setExpanded(!expanded)
36 | }
37 |
38 | function castMonthToNumber(month: string) {
39 | switch (month) {
40 | case "JANUARY":
41 | return "01."
42 | case "FEBRUARY":
43 | return "02."
44 | case "MARCH":
45 | return "03."
46 | case "APRIL":
47 | return "04."
48 | case "MAY":
49 | return "05."
50 | case "JUNE":
51 | return "06."
52 | case "JULY":
53 | return "07."
54 | case "AUGUST":
55 | return "08."
56 | case "SEPTEMBER":
57 | return "09."
58 | case "OCTOBER":
59 | return "10."
60 | case "NOVEMBER":
61 | return "11."
62 | case "DECEMBER":
63 | return "12."
64 | }
65 | }
66 |
67 | const customDate = props.entry.date.split("-")[2] + "." + castMonthToNumber(props.monthYear.split("-")[0])
68 |
69 | return (
70 | <>
71 |
72 |
73 |
74 |
75 |
76 | {props.entry.title}
77 |
78 |
79 | {props.entry.amount}€
80 |
81 |
82 |
83 | {customDate}
84 |
85 |
86 |
87 |
88 | {expanded ? : }
89 |
90 |
91 | {expanded && (
92 |
93 |
94 |
95 | Description:
96 |
97 |
98 | {props.entry.description}
99 |
100 |
101 |
102 |
103 | Interval:
104 |
105 |
106 | {props.entry.interval}
107 |
108 |
109 |
110 |
111 | Cost Type:
112 |
113 |
114 | {props.entry.costType}
115 |
116 |
117 |
118 | )}
119 |
120 |
121 | >
122 | )
123 | }
124 |
125 | const StyledCard = styled(Card)`
126 | min-width: 275px;
127 | margin: 16px;
128 | background: #edf0fc;
129 | box-shadow: 10px 10px 5px -4px silver;;`
130 | const StyledCardContent = styled(CardContent)`
131 | display: flex;
132 | flex-direction: column;;`
133 | const RegularCardContent = styled.div`
134 | display: flex;
135 | justify-content: space-between;
136 | flex-direction: row;;`
137 |
138 | const ExpandedCardContent = styled.div`
139 | display: flex;
140 | flex-direction: column;
141 | ;`
142 |
143 | const ButtonDiv = styled.div`
144 | display: flex;
145 | flex-direction: row;
146 | justify-content: space-between;
147 | `;
148 |
149 | interface AmountTypographyProps {
150 | entry: {
151 | category: Category;
152 | };
153 | }
154 |
155 | const AmountTypography = styled(Typography)`
156 | color: ${props => (props.entry?.category === 'EXPENSE' ? 'red' : 'green')};;`
157 |
158 | const DescriptionTypography = styled(Typography)`
159 | color: black;
160 | font-family: "Roboto Light", sans-serif;
161 | font-size: 16px;
162 | `;
163 | const DescriptionDiv = styled.div`
164 | display: flex;
165 | flex-direction: row;
166 | justify-content: space-between;
167 | `;
168 |
169 | const IntervalTypography = styled(Typography)`
170 | color: black;
171 | font-family: "Roboto Light", sans-serif;
172 | font-size: 16px;
173 | `;
174 |
175 | const IntervalDiv = styled.div`
176 | display: flex;
177 | flex-direction: row;
178 | justify-content: space-between;
179 | `;
180 |
181 | const CosttypeTypography = styled(Typography)`
182 | color: black;
183 | font-family: "Roboto Light", sans-serif;
184 | font-size: 16px;
185 | `;
186 |
187 | const CosttypeDiv = styled.div`
188 | display: flex;
189 | flex-direction: row;
190 | justify-content: space-between;
191 | `;
192 |
--------------------------------------------------------------------------------
/backend/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
30 | @REM e.g. to debug Maven itself, use
31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
33 | @REM ----------------------------------------------------------------------------
34 |
35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
36 | @echo off
37 | @REM set title of command window
38 | title %0
39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
41 |
42 | @REM set %HOME% to equivalent of $HOME
43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
44 |
45 | @REM Execute a user defined script before this one
46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
50 | :skipRcPre
51 |
52 | @setlocal
53 |
54 | set ERROR_CODE=0
55 |
56 | @REM To isolate internal variables from possible post scripts, we use another setlocal
57 | @setlocal
58 |
59 | @REM ==== START VALIDATION ====
60 | if not "%JAVA_HOME%" == "" goto OkJHome
61 |
62 | echo.
63 | echo Error: JAVA_HOME not found in your environment. >&2
64 | echo Please set the JAVA_HOME variable in your environment to match the >&2
65 | echo location of your Java installation. >&2
66 | echo.
67 | goto error
68 |
69 | :OkJHome
70 | if exist "%JAVA_HOME%\bin\java.exe" goto init
71 |
72 | echo.
73 | echo Error: JAVA_HOME is set to an invalid directory. >&2
74 | echo JAVA_HOME = "%JAVA_HOME%" >&2
75 | echo Please set the JAVA_HOME variable in your environment to match the >&2
76 | echo location of your Java installation. >&2
77 | echo.
78 | goto error
79 |
80 | @REM ==== END VALIDATION ====
81 |
82 | :init
83 |
84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
85 | @REM Fallback to current working directory if not found.
86 |
87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
89 |
90 | set EXEC_DIR=%CD%
91 | set WDIR=%EXEC_DIR%
92 | :findBaseDir
93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
94 | cd ..
95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
96 | set WDIR=%CD%
97 | goto findBaseDir
98 |
99 | :baseDirFound
100 | set MAVEN_PROJECTBASEDIR=%WDIR%
101 | cd "%EXEC_DIR%"
102 | goto endDetectBaseDir
103 |
104 | :baseDirNotFound
105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
106 | cd "%EXEC_DIR%"
107 |
108 | :endDetectBaseDir
109 |
110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
111 |
112 | @setlocal EnableExtensions EnableDelayedExpansion
113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
115 |
116 | :endReadAdditionalConfig
117 |
118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
121 |
122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
123 |
124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
126 | )
127 |
128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
130 | if exist %WRAPPER_JAR% (
131 | if "%MVNW_VERBOSE%" == "true" (
132 | echo Found %WRAPPER_JAR%
133 | )
134 | ) else (
135 | if not "%MVNW_REPOURL%" == "" (
136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
137 | )
138 | if "%MVNW_VERBOSE%" == "true" (
139 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
140 | echo Downloading from: %WRAPPER_URL%
141 | )
142 |
143 | powershell -Command "&{"^
144 | "$webclient = new-object System.Net.WebClient;"^
145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
147 | "}"^
148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
149 | "}"
150 | if "%MVNW_VERBOSE%" == "true" (
151 | echo Finished downloading %WRAPPER_JAR%
152 | )
153 | )
154 | @REM End of extension
155 |
156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
157 | SET WRAPPER_SHA_256_SUM=""
158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
160 | )
161 | IF NOT %WRAPPER_SHA_256_SUM%=="" (
162 | powershell -Command "&{"^
163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
168 | " exit 1;"^
169 | "}"^
170 | "}"
171 | if ERRORLEVEL 1 goto error
172 | )
173 |
174 | @REM Provide a "standardized" way to retrieve the CLI args that will
175 | @REM work with both Windows and non-Windows executions.
176 | set MAVEN_CMD_LINE_ARGS=%*
177 |
178 | %MAVEN_JAVA_EXE% ^
179 | %JVM_CONFIG_MAVEN_PROPS% ^
180 | %MAVEN_OPTS% ^
181 | %MAVEN_DEBUG_OPTS% ^
182 | -classpath %WRAPPER_JAR% ^
183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
185 | if ERRORLEVEL 1 goto error
186 | goto end
187 |
188 | :error
189 | set ERROR_CODE=1
190 |
191 | :end
192 | @endlocal & set ERROR_CODE=%ERROR_CODE%
193 |
194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
198 | :skipRcPost
199 |
200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause
202 |
203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
204 |
205 | cmd /C exit /B %ERROR_CODE%
206 |
--------------------------------------------------------------------------------
/frontend/src/entryAddUpdate/EntryAddUpdate.tsx:
--------------------------------------------------------------------------------
1 | import styled from "@emotion/styled";
2 | import {
3 | Button,
4 | FormControl,
5 | InputLabel, MenuItem,
6 | Select,
7 | TextField,
8 | ToggleButton,
9 | ToggleButtonGroup
10 | } from "@mui/material";
11 | import React, {useState} from "react";
12 | import {Category} from "../model/Category.ts";
13 | import {Interval} from "../model/Interval.ts";
14 | import {useStore} from "../hooks/useStore.ts";
15 | import {useNavigate} from "react-router-dom";
16 | import {CostType} from "../model/CostType.ts";
17 |
18 |
19 | export default function EntryAddUpdate() {
20 |
21 | const getUpdatedCard = useStore((state) => state.getUpdatedCard)
22 | const updatedCard = getUpdatedCard()
23 | const getIsCardUpdated = useStore((state) => state.getIsCardUpdated)
24 |
25 | const [interval, setInterval] = useState(updatedCard && getIsCardUpdated() ? updatedCard?.interval : 'ONCE');
26 | const [title, setTitle] = useState(updatedCard && getIsCardUpdated() ? updatedCard?.title : "")
27 | const [description, setDescription] = useState(updatedCard && getIsCardUpdated() ? updatedCard?.description : "")
28 | const [amount, setAmount] = useState(updatedCard && getIsCardUpdated() ? updatedCard?.amount : "")
29 | const [date, setDate] = useState(updatedCard && getIsCardUpdated() ? updatedCard?.date : "")
30 | const [category, setCategory] = useState(updatedCard && getIsCardUpdated() ? updatedCard.category : "INCOME")
31 | const [costType, setCostType] = useState(updatedCard && getIsCardUpdated() ? updatedCard.costType : "FIXED")
32 |
33 |
34 |
35 | const navigate = useNavigate()
36 | const setIsCardUpdated = useStore((state) => state.setIsCardUpdated)
37 |
38 | const createEntry = useStore((state) => state.createEntry)
39 | const updateEntry = useStore((state) => state.updateEntry)
40 | const deleteEntry = useStore((state) => state.deleteEntry)
41 |
42 |
43 | function resetAllUseStates() {
44 | setTitle("")
45 | setDescription("")
46 | setAmount("")
47 | setDate("")
48 | setInterval("ONCE")
49 | setCategory("INCOME")
50 | setCostType("FIXED")
51 | }
52 |
53 | function handleSubmit(event: React.FormEvent) {
54 | event.preventDefault()
55 | getIsCardUpdated() ? handlePut() : handlePost()
56 |
57 | }
58 |
59 | function handlePost() {
60 | const requestBody = {
61 | title: title,
62 | description: description,
63 | amount: amount.replace(/,/, "."),
64 | date: date,
65 | interval: interval,
66 | category: category,
67 | costType: costType
68 | }
69 | createEntry(requestBody)
70 | navigate(`/monthlyBalance`)
71 | setIsCardUpdated(false)
72 | resetAllUseStates()
73 |
74 | }
75 |
76 | function handlePut() {
77 |
78 | const requestBody = {
79 | title: title,
80 | description: description,
81 | amount: amount,
82 | date: date,
83 | interval: interval,
84 | category: category,
85 | costType: costType
86 | }
87 | updateEntry(requestBody, getUpdatedCard()?.id as string)
88 | navigate(`/monthlyBalance`)
89 | resetAllUseStates()
90 | setIsCardUpdated(false)
91 | }
92 |
93 | function handleDelete() {
94 | deleteEntry(getUpdatedCard()?.id as string)
95 | navigate(`/monthlyBalance`)
96 | resetAllUseStates()
97 | setIsCardUpdated(false)
98 | }
99 |
100 |
101 | function handleCancel() {
102 | navigate(`/monthlyBalance`)
103 | resetAllUseStates()
104 | setIsCardUpdated(false)
105 | }
106 |
107 | function handleChangeCategory(_: React.MouseEvent, newCategory: Category) {
108 | setCategory(newCategory)
109 | }
110 |
111 | function handleChangeCostType(_: React.MouseEvent, newCostType: CostType) {
112 | setCostType(newCostType)
113 | }
114 |
115 | return (
116 |
117 | Add Entry
118 |
119 |
120 |
121 | setTitle(e.target.value)}/>
123 | setDescription(e.target.value)}/>
125 | setAmount(e.target.value)}/>
128 | setDate(e.target.value)}/>
130 |
131 |
132 |
133 | Interval
134 | setInterval(e.target.value as Interval)}
137 | >
138 | Once
139 | Monthly
140 | Quarterly
141 | Half Yearly
142 | Yearly
143 |
144 |
145 |
146 |
149 | Income
150 | Expense
151 |
152 |
153 |
156 | Fixed
157 | Variable
158 |
159 |
160 |
161 | {getIsCardUpdated() ?
162 | (<>
163 | Save
164 | Delete >) :
165 | (Add )
166 | }
167 |
168 | Cancel
169 |
170 |
171 |
172 | )
173 | }
174 |
175 | const StyledForm = styled.form`
176 |
177 | `;
178 |
179 | const StyledDiv = styled.div`
180 | display: flex;
181 | flex-direction: column;
182 | margin: 16px;
183 | `;
184 | const StyledH2 = styled.h2`
185 | font-family: "Roboto Light", sans-serif;
186 | display: flex;
187 | justify-content: center;
188 | `;
189 |
190 | const StyledTextField = styled(TextField)`
191 | font-family: "Roboto Light", sans-serif;
192 | display: flex;
193 | justify-content: center;
194 | margin: 16px;
195 | `;
196 |
197 |
198 | const StyledToggleButton = styled(ToggleButton)`
199 | font-family: "Roboto Light", sans-serif;
200 | display: flex;
201 | justify-content: center;
202 |
203 | `;
204 |
205 | const StyledToggleGroup = styled(ToggleButtonGroup)`
206 | font-family: "Roboto Light", sans-serif;
207 | display: flex;
208 | justify-content: center;
209 | margin: 16px;
210 | `;
211 |
212 | const StyledButton = styled(Button)`
213 | font-family: "Roboto Light", sans-serif;
214 | display: flex;
215 | justify-content: center;
216 | margin: 16px;
217 | border: 1px solid black;
218 | width: 130px;
219 | `;
220 |
221 | const StyleDivButtons = styled.div`
222 | display: flex;
223 | flex-direction: row;
224 | justify-content: center;
225 | `;
226 |
--------------------------------------------------------------------------------
/backend/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # https://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Apache Maven Wrapper startup batch script, version 3.2.0
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | # e.g. to debug Maven itself, use
32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | # ----------------------------------------------------------------------------
35 |
36 | if [ -z "$MAVEN_SKIP_RC" ] ; then
37 |
38 | if [ -f /usr/local/etc/mavenrc ] ; then
39 | . /usr/local/etc/mavenrc
40 | fi
41 |
42 | if [ -f /etc/mavenrc ] ; then
43 | . /etc/mavenrc
44 | fi
45 |
46 | if [ -f "$HOME/.mavenrc" ] ; then
47 | . "$HOME/.mavenrc"
48 | fi
49 |
50 | fi
51 |
52 | # OS specific support. $var _must_ be set to either true or false.
53 | cygwin=false;
54 | darwin=false;
55 | mingw=false
56 | case "$(uname)" in
57 | CYGWIN*) cygwin=true ;;
58 | MINGW*) mingw=true;;
59 | Darwin*) darwin=true
60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
62 | if [ -z "$JAVA_HOME" ]; then
63 | if [ -x "/usr/libexec/java_home" ]; then
64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
65 | else
66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
67 | fi
68 | fi
69 | ;;
70 | esac
71 |
72 | if [ -z "$JAVA_HOME" ] ; then
73 | if [ -r /etc/gentoo-release ] ; then
74 | JAVA_HOME=$(java-config --jre-home)
75 | fi
76 | fi
77 |
78 | # For Cygwin, ensure paths are in UNIX format before anything is touched
79 | if $cygwin ; then
80 | [ -n "$JAVA_HOME" ] &&
81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
82 | [ -n "$CLASSPATH" ] &&
83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
84 | fi
85 |
86 | # For Mingw, ensure paths are in UNIX format before anything is touched
87 | if $mingw ; then
88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
90 | fi
91 |
92 | if [ -z "$JAVA_HOME" ]; then
93 | javaExecutable="$(which javac)"
94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
95 | # readlink(1) is not available as standard on Solaris 10.
96 | readLink=$(which readlink)
97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
98 | if $darwin ; then
99 | javaHome="$(dirname "\"$javaExecutable\"")"
100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
101 | else
102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")"
103 | fi
104 | javaHome="$(dirname "\"$javaExecutable\"")"
105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin')
106 | JAVA_HOME="$javaHome"
107 | export JAVA_HOME
108 | fi
109 | fi
110 | fi
111 |
112 | if [ -z "$JAVACMD" ] ; then
113 | if [ -n "$JAVA_HOME" ] ; then
114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
115 | # IBM's JDK on AIX uses strange locations for the executables
116 | JAVACMD="$JAVA_HOME/jre/sh/java"
117 | else
118 | JAVACMD="$JAVA_HOME/bin/java"
119 | fi
120 | else
121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
122 | fi
123 | fi
124 |
125 | if [ ! -x "$JAVACMD" ] ; then
126 | echo "Error: JAVA_HOME is not defined correctly." >&2
127 | echo " We cannot execute $JAVACMD" >&2
128 | exit 1
129 | fi
130 |
131 | if [ -z "$JAVA_HOME" ] ; then
132 | echo "Warning: JAVA_HOME environment variable is not set."
133 | fi
134 |
135 | # traverses directory structure from process work directory to filesystem root
136 | # first directory with .mvn subdirectory is considered project base directory
137 | find_maven_basedir() {
138 | if [ -z "$1" ]
139 | then
140 | echo "Path not specified to find_maven_basedir"
141 | return 1
142 | fi
143 |
144 | basedir="$1"
145 | wdir="$1"
146 | while [ "$wdir" != '/' ] ; do
147 | if [ -d "$wdir"/.mvn ] ; then
148 | basedir=$wdir
149 | break
150 | fi
151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
152 | if [ -d "${wdir}" ]; then
153 | wdir=$(cd "$wdir/.." || exit 1; pwd)
154 | fi
155 | # end of workaround
156 | done
157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)"
158 | }
159 |
160 | # concatenates all lines of a file
161 | concat_lines() {
162 | if [ -f "$1" ]; then
163 | # Remove \r in case we run on Windows within Git Bash
164 | # and check out the repository with auto CRLF management
165 | # enabled. Otherwise, we may read lines that are delimited with
166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
167 | # splitting rules.
168 | tr -s '\r\n' ' ' < "$1"
169 | fi
170 | }
171 |
172 | log() {
173 | if [ "$MVNW_VERBOSE" = true ]; then
174 | printf '%s\n' "$1"
175 | fi
176 | }
177 |
178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
179 | if [ -z "$BASE_DIR" ]; then
180 | exit 1;
181 | fi
182 |
183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
184 | log "$MAVEN_PROJECTBASEDIR"
185 |
186 | ##########################################################################################
187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
188 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
189 | ##########################################################################################
190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
191 | if [ -r "$wrapperJarPath" ]; then
192 | log "Found $wrapperJarPath"
193 | else
194 | log "Couldn't find $wrapperJarPath, downloading it ..."
195 |
196 | if [ -n "$MVNW_REPOURL" ]; then
197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
198 | else
199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
200 | fi
201 | while IFS="=" read -r key value; do
202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
203 | safeValue=$(echo "$value" | tr -d '\r')
204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
205 | esac
206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
207 | log "Downloading from: $wrapperUrl"
208 |
209 | if $cygwin; then
210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
211 | fi
212 |
213 | if command -v wget > /dev/null; then
214 | log "Found wget ... using wget"
215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
218 | else
219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
220 | fi
221 | elif command -v curl > /dev/null; then
222 | log "Found curl ... using curl"
223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
226 | else
227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
228 | fi
229 | else
230 | log "Falling back to using Java to download"
231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
233 | # For Cygwin, switch paths to Windows format before running javac
234 | if $cygwin; then
235 | javaSource=$(cygpath --path --windows "$javaSource")
236 | javaClass=$(cygpath --path --windows "$javaClass")
237 | fi
238 | if [ -e "$javaSource" ]; then
239 | if [ ! -e "$javaClass" ]; then
240 | log " - Compiling MavenWrapperDownloader.java ..."
241 | ("$JAVA_HOME/bin/javac" "$javaSource")
242 | fi
243 | if [ -e "$javaClass" ]; then
244 | log " - Running MavenWrapperDownloader.java ..."
245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
246 | fi
247 | fi
248 | fi
249 | fi
250 | ##########################################################################################
251 | # End of extension
252 | ##########################################################################################
253 |
254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file
255 | wrapperSha256Sum=""
256 | while IFS="=" read -r key value; do
257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
258 | esac
259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
260 | if [ -n "$wrapperSha256Sum" ]; then
261 | wrapperSha256Result=false
262 | if command -v sha256sum > /dev/null; then
263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
264 | wrapperSha256Result=true
265 | fi
266 | elif command -v shasum > /dev/null; then
267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
268 | wrapperSha256Result=true
269 | fi
270 | else
271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
273 | exit 1
274 | fi
275 | if [ $wrapperSha256Result = false ]; then
276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
279 | exit 1
280 | fi
281 | fi
282 |
283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
284 |
285 | # For Cygwin, switch paths to Windows format before running java
286 | if $cygwin; then
287 | [ -n "$JAVA_HOME" ] &&
288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
289 | [ -n "$CLASSPATH" ] &&
290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
291 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
293 | fi
294 |
295 | # Provide a "standardized" way to retrieve the CLI args that will
296 | # work with both Windows and non-Windows executions.
297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
298 | export MAVEN_CMD_LINE_ARGS
299 |
300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
301 |
302 | # shellcheck disable=SC2086 # safe args
303 | exec "$JAVACMD" \
304 | $MAVEN_OPTS \
305 | $MAVEN_DEBUG_OPTS \
306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
309 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/monthlybalance/MonthlySortTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.monthlybalance;
2 |
3 | import de.neuefische.capstone.backend.entries.EntriesRepo;
4 | import de.neuefische.capstone.backend.model.*;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.math.BigDecimal;
8 | import java.time.LocalDate;
9 |
10 | import java.util.*;
11 |
12 |
13 | import static org.assertj.core.api.Assertions.assertThat;
14 |
15 | import static org.mockito.Mockito.*;
16 |
17 | class MonthlySortTest {
18 |
19 | EntriesRepo entriesRepo = mock(EntriesRepo.class);
20 |
21 | MonthlySort monthlySort = new MonthlySort(entriesRepo);
22 |
23 | LocalDate currentDate = LocalDate.now();
24 | int currentMonth = currentDate.getMonthValue();
25 | int currentYear = currentDate.getYear();
26 | int monthInOneYear = currentMonth + 12;
27 |
28 | boolean calculateApplicableMonths(int interval, int startMonth, int targetMonth) {
29 | List applicableMonths = new ArrayList<>();
30 | applicableMonths.add(startMonth);
31 | for(int i = 0; i <= interval; i++){
32 | startMonth = startMonth + interval;
33 | if (startMonth > 12){
34 | applicableMonths.add(startMonth - 12);
35 |
36 | }else {
37 | applicableMonths.add(startMonth);
38 | }
39 | }
40 |
41 | return applicableMonths.contains(targetMonth);
42 |
43 | }
44 | @Test
45 | void whenEarliestMonthIsAugust23AndLatestAugust24generateMonthlyBalanceList() {
46 | //Given
47 |
48 |
49 | Entry earliestEntry = new Entry("1",
50 | "testTitle",
51 | "testDescription",
52 | LocalDate.of(currentYear, currentMonth, 22),
53 | new BigDecimal(1000),
54 | Category.INCOME,
55 | Interval.MONTHLY,
56 | CostType.FIXED);
57 |
58 | List entries = List.of(
59 | new Entry("1",
60 | "testTitle",
61 | "testDescription",
62 | LocalDate.of(currentYear, currentMonth, 22),
63 | new BigDecimal(1000),
64 | Category.INCOME,
65 | Interval.MONTHLY,
66 | CostType.FIXED));
67 |
68 | //When
69 | when(entriesRepo.findFirstByOrderByDateAsc()).thenReturn(earliestEntry);
70 | when(entriesRepo.findAll()).thenReturn(entries);
71 | Map actual = monthlySort.generateMonthlyBalanceList();
72 | Map expected = new HashMap<>();
73 |
74 | for (int month = currentMonth; month < monthInOneYear; month++) {
75 | int magicMonth;
76 | int year;
77 | if (month <= 12) {
78 | magicMonth = month;
79 | year = currentYear;
80 | } else {
81 | magicMonth = month - 12;
82 | year = currentYear + 1;
83 | }
84 | String monthLabel = LocalDate.of(2023, magicMonth, 1).getMonth().toString().toUpperCase();
85 | monthLabel = monthLabel + "-" + year;
86 | MonthlyBalance monthlyBalance = new MonthlyBalance(monthLabel,
87 | new BigDecimal("1000"),
88 | new BigDecimal("0"),
89 | new BigDecimal("0"),
90 | new BigDecimal("0"),
91 | new BigDecimal("0"),
92 | new BigDecimal("1000"),
93 | new ArrayList<>(entries));
94 | expected.put(monthLabel, monthlyBalance);
95 | }
96 |
97 |
98 | //Then
99 | verify(entriesRepo).findFirstByOrderByDateAsc();
100 | verify(entriesRepo, times(36)).findAll();
101 | assertThat(actual).isEqualTo(expected);
102 | }
103 |
104 | @Test
105 | void whenEarliestMonthIsAugust23AndIntervalIsOnceGenerateMonthlyBalanceList() {
106 | //Given
107 | Entry earliestEntry = new Entry("1",
108 | "testTitle",
109 | "testDescription",
110 | LocalDate.of(currentYear, currentMonth, 22),
111 | new BigDecimal(1000),
112 | Category.INCOME,
113 | Interval.ONCE,
114 | CostType.FIXED);
115 |
116 | List entries = List.of(
117 | new Entry("1",
118 | "testTitle",
119 | "testDescription",
120 | LocalDate.of(currentYear, currentMonth, 22),
121 | new BigDecimal(1000),
122 | Category.INCOME,
123 | Interval.ONCE,
124 | CostType.FIXED));
125 |
126 | //When
127 | when(entriesRepo.findFirstByOrderByDateAsc()).thenReturn(earliestEntry);
128 | when(entriesRepo.findAll()).thenReturn(entries);
129 | Map actual = monthlySort.generateMonthlyBalanceList();
130 | Map expected = new HashMap<>();
131 |
132 | for (int month = currentMonth; month < monthInOneYear; month++) {
133 | int magicMonth;
134 | int year;
135 | if (month <= 12) {
136 | magicMonth = month;
137 | year = currentYear;
138 | } else {
139 | magicMonth = month - 12;
140 | year = currentYear + 1;
141 | }
142 | if (magicMonth==currentMonth) {
143 | String monthLabel = LocalDate.of(2023, magicMonth, 1).getMonth().toString().toUpperCase();
144 | monthLabel = monthLabel + "-" + year;
145 | MonthlyBalance monthlyBalance = new MonthlyBalance(monthLabel,
146 | new BigDecimal("1000"),
147 | new BigDecimal("0"),
148 | new BigDecimal("0"),
149 | new BigDecimal("0"),
150 | new BigDecimal("0"),
151 | new BigDecimal("1000"),
152 | new ArrayList<>(entries));
153 | expected.put(monthLabel, monthlyBalance);
154 | }else {
155 | String monthLabel = LocalDate.of(2023, magicMonth, 1).getMonth().toString().toUpperCase();
156 | monthLabel = monthLabel + "-" + year;
157 | MonthlyBalance monthlyBalance = new MonthlyBalance(monthLabel,
158 | new BigDecimal("0"),
159 | new BigDecimal("0"),
160 | new BigDecimal("0"),
161 | new BigDecimal("0"),
162 | new BigDecimal("0"),
163 | new BigDecimal("0"),
164 | new ArrayList<>());
165 | expected.put(monthLabel, monthlyBalance);
166 | }
167 | }
168 | verify(entriesRepo).findFirstByOrderByDateAsc();
169 | verify(entriesRepo, times(36)).findAll();
170 | assertThat(actual).isEqualTo(expected);
171 | }
172 |
173 | @Test
174 | void whenEarliestIsCurrentDateAndIntervalIsQuarterlyGenerateMonthlyBalanceList(){
175 | //Given
176 | int multiplierQuarterly = 3;
177 |
178 | Entry earliestEntry = new Entry("1",
179 | "testTitle",
180 | "testDescription",
181 | LocalDate.of(currentYear, currentMonth, 22),
182 | new BigDecimal(1000),
183 | Category.INCOME,
184 | Interval.QUARTERLY,
185 | CostType.FIXED);
186 |
187 | List entries = List.of(
188 | new Entry("1",
189 | "testTitle",
190 | "testDescription",
191 | LocalDate.of(currentYear, currentMonth, 22),
192 | new BigDecimal(1000),
193 | Category.INCOME,
194 | Interval.QUARTERLY,
195 | CostType.FIXED));
196 |
197 | //When
198 | when(entriesRepo.findFirstByOrderByDateAsc()).thenReturn(earliestEntry);
199 | when(entriesRepo.findAll()).thenReturn(entries);
200 | Map actual = monthlySort.generateMonthlyBalanceList();
201 | Map expected = new HashMap<>();
202 |
203 | for (int month = currentMonth; month < monthInOneYear; month++) {
204 | int magicMonth;
205 | int year;
206 | if (month <= 12) {
207 | magicMonth = month;
208 | year = currentYear;
209 | } else {
210 | magicMonth = month - 12;
211 | year = currentYear + 1;
212 | }
213 |
214 |
215 |
216 | if (magicMonth==currentMonth || calculateApplicableMonths(multiplierQuarterly, currentMonth, magicMonth)) {
217 | String monthLabel = LocalDate.of(2023, magicMonth, 1).getMonth().toString().toUpperCase();
218 | monthLabel = monthLabel + "-" + year;
219 | MonthlyBalance monthlyBalance = new MonthlyBalance(monthLabel,
220 | new BigDecimal("1000"),
221 | new BigDecimal("0"),
222 | new BigDecimal("0"),
223 | new BigDecimal("0"),
224 | new BigDecimal("0"),
225 | new BigDecimal("1000"),
226 | new ArrayList<>(entries));
227 | expected.put(monthLabel, monthlyBalance);
228 | }else {
229 | String monthLabel = LocalDate.of(currentYear, magicMonth, 1).getMonth().toString().toUpperCase();
230 | monthLabel = monthLabel + "-" + year;
231 | MonthlyBalance monthlyBalance = new MonthlyBalance(monthLabel,
232 | new BigDecimal("0"),
233 | new BigDecimal("0"),
234 | new BigDecimal("0"),
235 | new BigDecimal("0"),
236 | new BigDecimal("0"),
237 | new BigDecimal("0"),
238 | new ArrayList<>());
239 | expected.put(monthLabel, monthlyBalance);
240 | }
241 | }
242 | verify(entriesRepo).findFirstByOrderByDateAsc();
243 | verify(entriesRepo, times(36)).findAll();
244 | assertThat(actual).isEqualTo(expected);
245 | }
246 |
247 | @Test
248 | void whenEarliestIsCurrentDateAndIntervalIsHalfYearlyGenerateMonthlyBalanceList() {
249 | //Given
250 | int multiplierHalfYearly = 6;
251 | Entry earliestEntry = new Entry("1",
252 | "testTitle",
253 | "testDescription",
254 | LocalDate.of(currentYear, currentMonth, 22),
255 | new BigDecimal(1000),
256 | Category.INCOME,
257 | Interval.HALF_YEARLY,
258 | CostType.FIXED);
259 |
260 | List entries = List.of(
261 | new Entry("1",
262 | "testTitle",
263 | "testDescription",
264 | LocalDate.of(currentYear, currentMonth, 22),
265 | new BigDecimal(1000),
266 | Category.INCOME,
267 | Interval.HALF_YEARLY,
268 | CostType.FIXED));
269 |
270 | //When
271 | when(entriesRepo.findFirstByOrderByDateAsc()).thenReturn(earliestEntry);
272 | when(entriesRepo.findAll()).thenReturn(entries);
273 | Map actual = monthlySort.generateMonthlyBalanceList();
274 | Map expected = new HashMap<>();
275 |
276 | for (int month = currentMonth; month < monthInOneYear; month++) {
277 | int magicMonth;
278 | int year;
279 | if (month <= 12) {
280 | magicMonth = month;
281 | year = currentYear;
282 | } else {
283 | magicMonth = month - 12;
284 | year = currentYear + 1;
285 | }
286 | if (calculateApplicableMonths(multiplierHalfYearly, currentMonth, magicMonth)) {
287 | String monthLabel = LocalDate.of(currentYear, magicMonth, 1).getMonth().toString().toUpperCase();
288 | monthLabel = monthLabel + "-" + year;
289 | MonthlyBalance monthlyBalance = new MonthlyBalance(monthLabel,
290 | new BigDecimal("1000"),
291 | new BigDecimal("0"),
292 | new BigDecimal("0"),
293 | new BigDecimal("0"),
294 | new BigDecimal("0"),
295 | new BigDecimal("1000"),
296 | new ArrayList<>(entries));
297 | expected.put(monthLabel, monthlyBalance);
298 | }else {
299 | String monthLabel = LocalDate.of(currentYear, magicMonth, 1).getMonth().toString().toUpperCase();
300 | monthLabel = monthLabel + "-" + year;
301 | MonthlyBalance monthlyBalance = new MonthlyBalance(monthLabel,
302 | new BigDecimal("0"),
303 | new BigDecimal("0"),
304 | new BigDecimal("0"),
305 | new BigDecimal("0"),
306 | new BigDecimal("0"),
307 | new BigDecimal("0"),
308 | new ArrayList<>());
309 | expected.put(monthLabel, monthlyBalance);
310 | }
311 | }
312 | verify(entriesRepo).findFirstByOrderByDateAsc();
313 | verify(entriesRepo, times(36)).findAll();
314 | assertThat(actual).isEqualTo(expected);
315 | }
316 |
317 | @Test
318 | void whenEarliestIsCurrentDateAndIntervalIsYearlyGenerateMonthlyBalanceList(){
319 | //Given
320 | int multiplierHalfYearly = 12;
321 | Entry earliestEntry = new Entry("1",
322 | "testTitle",
323 | "testDescription",
324 | LocalDate.of(currentYear, currentMonth, 22),
325 | new BigDecimal(1000),
326 | Category.INCOME,
327 | Interval.YEARLY,
328 | CostType.FIXED);
329 |
330 | List entries = List.of(
331 | new Entry("1",
332 | "testTitle",
333 | "testDescription",
334 | LocalDate.of(currentYear, currentMonth, 22),
335 | new BigDecimal(1000),
336 | Category.INCOME,
337 | Interval.YEARLY,
338 | CostType.FIXED));
339 |
340 | //When
341 | when(entriesRepo.findFirstByOrderByDateAsc()).thenReturn(earliestEntry);
342 | when(entriesRepo.findAll()).thenReturn(entries);
343 | Map actual = monthlySort.generateMonthlyBalanceList();
344 | Map expected = new HashMap<>();
345 |
346 | for (int month = currentMonth; month < monthInOneYear; month++) {
347 | int magicMonth;
348 | int year;
349 | if (month <= 12) {
350 | magicMonth = month;
351 | year = currentYear;
352 | } else {
353 | magicMonth = month - 12;
354 | year = currentYear + 1;
355 | }
356 | if (calculateApplicableMonths(multiplierHalfYearly, currentMonth, magicMonth)) {
357 | String monthLabel = LocalDate.of(currentYear, magicMonth, 1).getMonth().toString().toUpperCase();
358 | monthLabel = monthLabel + "-" + year;
359 | MonthlyBalance monthlyBalance = new MonthlyBalance(monthLabel,
360 | new BigDecimal("1000"),
361 | new BigDecimal("0"),
362 | new BigDecimal("0"),
363 | new BigDecimal("0"),
364 | new BigDecimal("0"),
365 | new BigDecimal("1000"),
366 | new ArrayList<>(entries));
367 | expected.put(monthLabel, monthlyBalance);
368 | }else {
369 | String monthLabel = LocalDate.of(currentYear, magicMonth, 1).getMonth().toString().toUpperCase();
370 | monthLabel = monthLabel + "-" + year;
371 | MonthlyBalance monthlyBalance = new MonthlyBalance(monthLabel,
372 | new BigDecimal("0"),
373 | new BigDecimal("0"),
374 | new BigDecimal("0"),
375 | new BigDecimal("0"),
376 | new BigDecimal("0"),
377 | new BigDecimal("0"),
378 | new ArrayList<>());
379 | expected.put(monthLabel, monthlyBalance);
380 | }
381 | }
382 | verify(entriesRepo).findFirstByOrderByDateAsc();
383 | verify(entriesRepo, times(36)).findAll();
384 | assertThat(actual).isEqualTo(expected);
385 | }
386 | }
387 |
--------------------------------------------------------------------------------