From 76a658986bd79917a98ef0c9ba5eb2fa05268aa9 Mon Sep 17 00:00:00 2001 From: Miroslav Date: Sat, 30 Dec 2023 18:30:25 +0200 Subject: [PATCH] #2: Display categories on transactions page --- .idea/dataSources.xml | 4 +- .idea/misc.xml | 2 +- APIGateway/src/main/resources/application.yml | 10 +-- .../ProcessedTransactionRepository.java | 3 + .../TransactionCategoryRepository.java | 10 +++ .../services/ProcessedTransactionService.java | 11 ++- .../finances/services/StatementsService.java | 38 ++++++++-- frontend/package-lock.json | 30 ++++++++ frontend/package.json | 1 + frontend/src/app/pages/CategoriesPage.jsx | 76 +++++++++++-------- frontend/src/app/pages/TransactionsPage.jsx | 20 ++++- .../components/categories/CategoriesBox.jsx | 61 +++++++++++++++ 12 files changed, 215 insertions(+), 51 deletions(-) create mode 100644 frontend/src/components/categories/CategoriesBox.jsx 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