├── 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 | [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=kajochi_capstone-budgetary-control-frontend&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=kajochi_capstone-budgetary-control-frontend) 4 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=kajochi_capstone-budgetary-control-backend&metric=coverage)](https://sonarcloud.io/summary/new_code?id=kajochi_capstone-budgetary-control-backend) 5 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=kajochi_capstone-budgetary-control-frontend&metric=alert_status)](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 | 69 | {options.map((option) => ( 70 | 71 | {option} 72 | 73 | ))} 74 | 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 | 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 | 53 | 54 | 55 | 56 | Choose a month 57 | 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 | 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 | --------------------------------------------------------------------------------