diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
index 6b53e14..2e17bc7 100644
--- a/.idea/dataSources.xml
+++ b/.idea/dataSources.xml
@@ -1,11 +1,11 @@
-
+
postgresql
true
org.postgresql.Driver
- jdbc:postgresql://localhost:5432/finances
+ jdbc:postgresql://localhost:5432/
diff --git a/.idea/misc.xml b/.idea/misc.xml
index ef46287..32d884a 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -4,7 +4,7 @@
-
+
\ No newline at end of file
diff --git a/APIGateway/src/main/resources/application.yml b/APIGateway/src/main/resources/application.yml
index 1e4a146..c7ffe05 100644
--- a/APIGateway/src/main/resources/application.yml
+++ b/APIGateway/src/main/resources/application.yml
@@ -41,11 +41,11 @@ spring:
- Path=/**
server:
ssl:
- enabled: true
- key-store-type: PKCS12
- key-store: classpath:keystore/local.p12
- key-store-password: asdf1234
- key-alias: local
+ enabled: ${SSL_ENABLED}
+ key-store-type: ${SSL_KEY_STORE_TYPE}
+ key-store: ${SSL_KEY_STORE}
+ key-store-password: ${SSL_KEY_STORE_PASSWORD}
+ key-alias: ${SSL_KEY_ALIAS}
reactive:
session:
cookie:
diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/ProcessedTransactionRepository.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/ProcessedTransactionRepository.java
index f01d668..57497b3 100644
--- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/ProcessedTransactionRepository.java
+++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/ProcessedTransactionRepository.java
@@ -1,6 +1,9 @@
package dev.mvvasilev.finances.persistence;
+import dev.mvvasilev.finances.dtos.TransactionCategoryDTO;
import dev.mvvasilev.finances.entity.ProcessedTransaction;
+import dev.mvvasilev.finances.entity.ProcessedTransactionCategory;
+import dev.mvvasilev.finances.entity.TransactionCategory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/TransactionCategoryRepository.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/TransactionCategoryRepository.java
index 116665c..24818a1 100644
--- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/TransactionCategoryRepository.java
+++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/TransactionCategoryRepository.java
@@ -28,4 +28,14 @@ public interface TransactionCategoryRepository extends JpaRepository fetchCategoriesForTransaction(@Param("transactionId") Long transactionId);
}
diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/ProcessedTransactionService.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/ProcessedTransactionService.java
index 04f2ba5..5ad05a3 100644
--- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/ProcessedTransactionService.java
+++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/ProcessedTransactionService.java
@@ -1,8 +1,10 @@
package dev.mvvasilev.finances.services;
import dev.mvvasilev.finances.dtos.ProcessedTransactionDTO;
+import dev.mvvasilev.finances.dtos.TransactionCategoryDTO;
import dev.mvvasilev.finances.entity.ProcessedTransaction;
import dev.mvvasilev.finances.persistence.ProcessedTransactionRepository;
+import dev.mvvasilev.finances.persistence.TransactionCategoryRepository;
import org.apache.commons.compress.utils.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
@@ -15,9 +17,12 @@ public class ProcessedTransactionService {
final private ProcessedTransactionRepository processedTransactionRepository;
+ final private TransactionCategoryRepository transactionCategoryRepository;
+
@Autowired
- public ProcessedTransactionService(ProcessedTransactionRepository processedTransactionRepository) {
+ public ProcessedTransactionService(ProcessedTransactionRepository processedTransactionRepository, TransactionCategoryRepository transactionCategoryRepository) {
this.processedTransactionRepository = processedTransactionRepository;
+ this.transactionCategoryRepository = transactionCategoryRepository;
}
public Page fetchPagedProcessedTransactionsForUser(int userId, final Pageable pageable) {
@@ -28,7 +33,9 @@ public class ProcessedTransactionService {
t.isInflow(),
t.getTimestamp(),
t.getDescription(),
- Lists.newArrayList() // TODO: Fetch categories. Do it all in SQL for better performance.
+ transactionCategoryRepository.fetchCategoriesForTransaction(t.getId())
+ .stream().map(ptc -> new TransactionCategoryDTO(ptc.getId(), ptc.getName()))
+ .toList()
));
}
}
diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatementsService.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatementsService.java
index e26d269..4785321 100644
--- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatementsService.java
+++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatementsService.java
@@ -20,6 +20,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
+import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
@@ -119,6 +120,7 @@ public class StatementsService {
// turn each cell in each row into a value, related to the value group ( column )
for (var group : valueGroups) {
+ var groupType = group.getType();
var valueList = new ArrayList();
for (int y = 1; y < lastRowIndex; y++) {
@@ -127,14 +129,34 @@ public class StatementsService {
value.setGroupId(group.getId());
value.setRowIndex(y);
- switch (group.getType()) {
- case STRING -> value.setStringValue(firstWorksheet.getRow(y).getCell(column).getStringCellValue());
- case NUMERIC ->
- value.setNumericValue(firstWorksheet.getRow(y).getCell(column).getNumericCellValue());
- case TIMESTAMP ->
- value.setTimestampValue(LocalDateTime.parse(firstWorksheet.getRow(y).getCell(column).getStringCellValue().trim(), DATE_FORMAT));
- case BOOLEAN ->
- value.setBooleanValue(firstWorksheet.getRow(y).getCell(column).getBooleanCellValue());
+ try {
+ var cellValue = firstWorksheet.getRow(y).getCell(column).getStringCellValue().trim();
+
+ try {
+ switch (groupType) {
+ case STRING -> value.setStringValue(cellValue);
+ case NUMERIC -> value.setNumericValue(Double.parseDouble(cellValue));
+ case TIMESTAMP -> value.setTimestampValue(LocalDateTime.parse(cellValue, DATE_FORMAT));
+ case BOOLEAN -> value.setBooleanValue(Boolean.parseBoolean(cellValue));
+ }
+ } catch (Exception e) {
+ switch (groupType) {
+ case STRING -> value.setStringValue("");
+ case NUMERIC -> value.setNumericValue(0.0);
+ case TIMESTAMP -> value.setTimestampValue(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC));
+ case BOOLEAN -> value.setBooleanValue(false);
+ }
+ }
+ } catch (IllegalStateException e) {
+ // Cell was numeric
+ var cellValue = firstWorksheet.getRow(y).getCell(column).getNumericCellValue();
+
+ switch (groupType) {
+ case STRING -> value.setStringValue(Double.toString(cellValue));
+ case NUMERIC -> value.setNumericValue(cellValue);
+ case TIMESTAMP -> value.setTimestampValue(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC));
+ case BOOLEAN -> value.setBooleanValue(false);
+ }
}
valueList.add(value);
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 8f6a7f0..303d5fe 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15,6 +15,7 @@
"@mui/material": "^5.15.0",
"@mui/x-data-grid": "^6.18.6",
"@mui/x-date-pickers": "^6.18.6",
+ "@mui/x-tree-view": "^6.17.0",
"dayjs": "^1.11.10",
"dotenv": "^16.3.1",
"react": "^18.2.0",
@@ -1440,6 +1441,35 @@
}
}
},
+ "node_modules/@mui/x-tree-view": {
+ "version": "6.17.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-6.17.0.tgz",
+ "integrity": "sha512-09dc2D+Rjg2z8KOaxbUXyPi0aw7fm2jurEtV8Xw48xJ00joLWd5QJm1/v4CarEvaiyhTQzHImNqdgeJW8ZQB6g==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2",
+ "@mui/base": "^5.0.0-beta.20",
+ "@mui/utils": "^5.14.14",
+ "@types/react-transition-group": "^4.4.8",
+ "clsx": "^2.0.0",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.8.6",
+ "@mui/system": "^5.8.0",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 8753695..d5b22a8 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,6 +17,7 @@
"@mui/material": "^5.15.0",
"@mui/x-data-grid": "^6.18.6",
"@mui/x-date-pickers": "^6.18.6",
+ "@mui/x-tree-view": "^6.17.0",
"dayjs": "^1.11.10",
"dotenv": "^16.3.1",
"react": "^18.2.0",
diff --git a/frontend/src/app/pages/CategoriesPage.jsx b/frontend/src/app/pages/CategoriesPage.jsx
index ad0ffd0..bb59ce8 100644
--- a/frontend/src/app/pages/CategoriesPage.jsx
+++ b/frontend/src/app/pages/CategoriesPage.jsx
@@ -1,6 +1,4 @@
-import {TreeItem, TreeView} from "@mui/x-tree-view";
import {
- Delete,
Category as CategoryIcon,
Add as AddIcon,
Close as CloseIcon,
@@ -25,6 +23,7 @@ import {
} from "@mui/material";
import Box from "@mui/material/Box";
import CategorizationRulesEditor from "@/components/categories/CategorizationRulesEditor.jsx";
+import CategoriesBox from "@/components/categories/CategoriesBox.jsx";
export default function CategoriesPage() {
@@ -176,39 +175,52 @@ export default function CategoriesPage() {
-
- {
- categories.map(c => {
- let variant = (selectedCategory?.id ?? -1) === c.id ? "filled" : "outlined";
+ selectable
+ selected={selectedCategory}
+ onCategorySelect={(e, c) => setSelectedCategory({...c})}
+ onCategoryDelete={(e, c) => {
+ setSelectedCategory(c);
+ openConfirmDeleteCategoryModal(true);
+ }}
+ showDelete
+ />
+ {/**/}
+ {/* {*/}
+ {/* categories.map(c => {*/}
+ {/* let variant = (selectedCategory?.id ?? -1) === c.id ? "filled" : "outlined";*/}
- return (
- {
- setSelectedCategory({...c});
- }}
- onDelete={() => {
- setSelectedCategory(c);
- openConfirmDeleteCategoryModal(true);
- }}
- label={c.name}
- deleteIcon={}
- variant={variant}
- />
- );
- })
- }
-
+ {/* return (*/}
+ {/* {*/}
+ {/* setSelectedCategory({...c});*/}
+ {/* }}*/}
+ {/* onDelete={() => {*/}
+ {/* setSelectedCategory(c);*/}
+ {/* openConfirmDeleteCategoryModal(true);*/}
+ {/* }}*/}
+ {/* label={c.name}*/}
+ {/* deleteIcon={}*/}
+ {/* variant={variant}*/}
+ {/* />*/}
+ {/* );*/}
+ {/* })*/}
+ {/* }*/}
+ {/**/}
diff --git a/frontend/src/app/pages/TransactionsPage.jsx b/frontend/src/app/pages/TransactionsPage.jsx
index 1cb52f8..1a915d9 100644
--- a/frontend/src/app/pages/TransactionsPage.jsx
+++ b/frontend/src/app/pages/TransactionsPage.jsx
@@ -4,6 +4,7 @@ import {useEffect, useState} from "react";
import {DataGrid} from "@mui/x-data-grid";
import utils from "@/utils.js";
import {ArrowDownward, ArrowUpward, PriceChange} from "@mui/icons-material";
+import CategoriesBox from "@/components/categories/CategoriesBox.jsx";
const COLUMNS = [
{
@@ -56,6 +57,23 @@ const COLUMNS = [
filterable: false,
valueFormatter: val => new Date(val.value).toLocaleString("bg-BG")
},
+ {
+ field: "categories",
+ headerName: "Categories",
+ maxWidth: 300,
+ flex: true,
+ sortable: false,
+ filterable: false,
+ renderCell: (params) => {
+ return (
+
+ );
+ }
+ }
];
export default function TransactionsPage() {
@@ -99,7 +117,7 @@ export default function TransactionsPage() {
>
{},
+ onCategoryDelete: onCategoryDelete = undefined,
+ showDelete: showDelete = false,
+ sx: sx = {},
+ minHeight: minHeight = "100px",
+ maxHeight: maxHeight = "250px",
+}) {
+ return (
+
+ {
+ categories.map(c => {
+ let variant = selectable && (selected?.id ?? -1) === c.id ? "filled" : "outlined";
+ let isDeletable = onCategoryDelete !== undefined;
+
+ return (
+ <>
+ {
+ isDeletable &&
+ onCategorySelect(e, c)}
+ onDelete={(e) => onCategoryDelete(e, c)}
+ label={c.name}
+ deleteIcon={showDelete === true ? : ""}
+ variant={variant}
+ />
+ }
+ {
+ !isDeletable &&
+ onCategorySelect(e, c)}
+ label={c.name}
+ variant={variant}
+ />
+ }
+ >
+ );
+ })
+ }
+
+ );
+}
\ No newline at end of file