diff --git a/Common/src/main/java/dev/mvvasilev/common/data/DataNamingStrategy.java b/Common/src/main/java/dev/mvvasilev/common/data/DataNamingStrategy.java deleted file mode 100644 index 6c50b76..0000000 --- a/Common/src/main/java/dev/mvvasilev/common/data/DataNamingStrategy.java +++ /dev/null @@ -1,4 +0,0 @@ -package dev.mvvasilev.common.data; - -public class DataNamingStrategy { -} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/advice/APIResponseAdvice.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/advice/APIResponseAdvice.java index 586c0d0..4a48c0c 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/advice/APIResponseAdvice.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/advice/APIResponseAdvice.java @@ -21,7 +21,7 @@ public class APIResponseAdvice { private Logger logger = LoggerFactory.getLogger(this.getClass()); @ExceptionHandler(Exception.class) - public APIResponseDTO processGenericException(Exception ex) { + public ResponseEntity> processGenericException(Exception ex) { List errors = List.of( new APIErrorDTO( ex.getMessage(), @@ -32,11 +32,12 @@ public class APIResponseAdvice { logger.error("Exception", ex); - return new APIResponseDTO<>( - null, - errors, - HttpStatus.INTERNAL_SERVER_ERROR.value(), - HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase() - ); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new APIResponseDTO<>( + null, + errors, + HttpStatus.INTERNAL_SERVER_ERROR.value(), + HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase() + )); } } diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/StatementsController.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/StatementsController.java new file mode 100644 index 0000000..fa8baa0 --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/StatementsController.java @@ -0,0 +1,62 @@ +package dev.mvvasilev.finances.controllers; + +import dev.mvvasilev.common.controller.AbstractRestController; +import dev.mvvasilev.common.web.APIResponseDTO; +import dev.mvvasilev.finances.dtos.TransactionValueGroupDTO; +import dev.mvvasilev.finances.dtos.UploadedStatementDTO; +import dev.mvvasilev.finances.services.StatementsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Collection; + +@RestController +@RequestMapping("/statements") +public class StatementsController extends AbstractRestController { + + private StatementsService statementsService; + + @Autowired + public StatementsController(StatementsService statementsService) { + this.statementsService = statementsService; + } + + @GetMapping + public ResponseEntity>> fetchStatements(Authentication authentication) { + return ResponseEntity.ofNullable( + ok(statementsService.fetchStatementsForUser(Integer.parseInt(authentication.getName()))) + ); + } + + @GetMapping("/{statementId}/transactionValueGroups") + public ResponseEntity>> fetchTransactionValueGroups(@PathVariable("statementId") Long statementId, Authentication authentication) { + return ResponseEntity.ofNullable(ok( + statementsService.fetchTransactionValueGroupsForUserStatement(statementId, Integer.parseInt(authentication.getName())) + )); + } + + @DeleteMapping("/{statementId}") + public ResponseEntity deleteStatement(@PathVariable("statementId") Long statementId, Authentication authentication) { + statementsService.deleteStatement(statementId, Integer.parseInt(authentication.getName())); + return ResponseEntity.ofNullable(ok(null)); + } + + @PostMapping(value = "/uploadSheet", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> uploadStatement(@RequestParam("file") MultipartFile file, Authentication authentication) throws IOException { + statementsService.uploadStatementFromExcelSheetForUser( + file.getOriginalFilename(), + file.getContentType(), + file.getInputStream(), + Integer.parseInt(authentication.getName()) + ); + return ResponseEntity.ofNullable(ok(1)); + } + +} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/TransactionsController.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/TransactionsController.java deleted file mode 100644 index f970596..0000000 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/TransactionsController.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.mvvasilev.finances.controllers; - -import dev.mvvasilev.common.controller.AbstractRestController; -import dev.mvvasilev.common.web.APIResponseDTO; -import dev.mvvasilev.finances.services.TransactionsService; -import org.apache.poi.ss.usermodel.WorkbookFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; - -@RestController("/transactions") -public class TransactionsController extends AbstractRestController { - - private TransactionsService transactionsService; - - @Autowired - public TransactionsController(TransactionsService transactionsService) { - this.transactionsService = transactionsService; - } - - @PostMapping(value = "/transactions/uploadSheet", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity> uploadTransactions(@RequestParam("file") MultipartFile file, Authentication authentication) throws IOException { - transactionsService.uploadMultipleTransactionsFromExcelSheetForUser( - file.getInputStream(), - authentication.getName() - ); - return ResponseEntity.ofNullable(ok(1)); - } - -} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/TransactionValueGroupDTO.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/TransactionValueGroupDTO.java new file mode 100644 index 0000000..7799925 --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/TransactionValueGroupDTO.java @@ -0,0 +1,9 @@ +package dev.mvvasilev.finances.dtos; + +import dev.mvvasilev.finances.enums.RawTransactionValueType; + +public record TransactionValueGroupDTO( + Long id, + String name, + RawTransactionValueType type +) {} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/UploadedStatementDTO.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/UploadedStatementDTO.java new file mode 100644 index 0000000..b4572ae --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/UploadedStatementDTO.java @@ -0,0 +1,9 @@ +package dev.mvvasilev.finances.dtos; + +import java.time.LocalDateTime; + +public record UploadedStatementDTO ( + Long id, + String name, + LocalDateTime timeUploaded +) {} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/Account.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/Account.java deleted file mode 100644 index 1460e80..0000000 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/Account.java +++ /dev/null @@ -1,26 +0,0 @@ -package dev.mvvasilev.finances.entity; - -public class Account { - - private String name; - - private long userId; - - public Account () {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public long getUserId() { - return userId; - } - - public void setUserId(long userId) { - this.userId = userId; - } -} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/AccountTransaction.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/AccountTransaction.java deleted file mode 100644 index 158a462..0000000 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/AccountTransaction.java +++ /dev/null @@ -1,41 +0,0 @@ -package dev.mvvasilev.finances.entity; - -import dev.mvvasilev.finances.enums.TransactionType; - -import java.math.BigDecimal; - -public class AccountTransaction { - - private BigDecimal amount; - - private TransactionType type; - - private String reason; - - public AccountTransaction() { - } - - public BigDecimal getAmount() { - return amount; - } - - public void setAmount(BigDecimal amount) { - this.amount = amount; - } - - public TransactionType getType() { - return type; - } - - public void setType(TransactionType type) { - this.type = type; - } - - public String getReason() { - return reason; - } - - public void setReason(String reason) { - this.reason = reason; - } -} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/ProcessedTransaction.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/ProcessedTransaction.java new file mode 100644 index 0000000..008bc47 --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/ProcessedTransaction.java @@ -0,0 +1,85 @@ +package dev.mvvasilev.finances.entity; + +import dev.mvvasilev.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import java.time.LocalDateTime; + +@Entity +@Table(schema = "transactions") +public class ProcessedTransaction extends AbstractEntity { + + private String description; + + private Integer userId; + + private Double amount; + + private boolean isInflow; + + private Long categoryId; + + private LocalDateTime timestamp; + + // private Long transactionMappingId; + + public ProcessedTransaction() { + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Double getAmount() { + return amount; + } + + public void setAmount(Double amount) { + this.amount = amount; + } + + public boolean isInflow() { + return isInflow; + } + + public void setInflow(boolean inflow) { + isInflow = inflow; + } + + public Long getCategoryId() { + return categoryId; + } + + public void setCategoryId(Long categoryId) { + this.categoryId = categoryId; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + +// public Long getTransactionMappingId() { +// return transactionMappingId; +// } +// +// public void setTransactionMappingId(Long transactionMappingId) { +// this.transactionMappingId = transactionMappingId; +// } +} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/RawStatement.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/RawStatement.java index 39c8acf..539914b 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/RawStatement.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/RawStatement.java @@ -10,6 +10,8 @@ public class RawStatement extends AbstractEntity { private Integer userId; + private String name; + public RawStatement() { } @@ -20,4 +22,12 @@ public class RawStatement extends AbstractEntity { public void setUserId(Integer userId) { this.userId = userId; } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/RawTransactionValue.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/RawTransactionValue.java index 7c7cf21..1d2ea4e 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/RawTransactionValue.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/RawTransactionValue.java @@ -19,6 +19,8 @@ public class RawTransactionValue extends AbstractEntity { private Boolean booleanValue; + private int rowIndex; + public RawTransactionValue() { } @@ -61,4 +63,12 @@ public class RawTransactionValue extends AbstractEntity { public void setBooleanValue(Boolean booleanValue) { this.booleanValue = booleanValue; } + + public int getRowIndex() { + return rowIndex; + } + + public void setRowIndex(int rowIndex) { + this.rowIndex = rowIndex; + } } diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/TransactionCategory.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/TransactionCategory.java new file mode 100644 index 0000000..cca3d61 --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/TransactionCategory.java @@ -0,0 +1,33 @@ +package dev.mvvasilev.finances.entity; + +import dev.mvvasilev.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(schema = "categories") +public class TransactionCategory extends AbstractEntity { + + private Integer userId; + + private String name; + + public TransactionCategory() { + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/TransactionMapping.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/TransactionMapping.java new file mode 100644 index 0000000..5546011 --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/TransactionMapping.java @@ -0,0 +1,15 @@ +package dev.mvvasilev.finances.entity; + +import dev.mvvasilev.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +//@Entity +//@Table(schema = "mappings") +//public class TransactionMapping extends AbstractEntity { +// +// private Long rawStatementId; +// +// +// +//} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/TransactionCategory.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/TransactionCategory.java deleted file mode 100644 index a37e25b..0000000 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/TransactionCategory.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.mvvasilev.finances.enums; - -public enum TransactionCategory { - Food, - Shopping, - Miscellaneous -} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/TransactionType.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/TransactionType.java deleted file mode 100644 index 0883103..0000000 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/TransactionType.java +++ /dev/null @@ -1,8 +0,0 @@ -package dev.mvvasilev.finances.enums; - -public enum TransactionType { - - INFLOW, - OUTFLOW - -} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/RawStatementRepository.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/RawStatementRepository.java index 44dc380..eb32690 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/RawStatementRepository.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/RawStatementRepository.java @@ -1,9 +1,27 @@ package dev.mvvasilev.finances.persistence; import dev.mvvasilev.finances.entity.RawStatement; +import dev.mvvasilev.finances.persistence.dtos.RawStatementDTO; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.Collection; + @Repository public interface RawStatementRepository extends JpaRepository { + + @Query(value = + "SELECT " + + "id, " + + "time_created as TimeCreated, " + + "time_last_modified as TimeLastModified, " + + "user_id as UserId, name " + + "FROM transactions.raw_statement " + + "WHERE user_id = :userId", + nativeQuery = true + ) + Collection fetchAllForUser(@Param("userId") int userId); + } diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/RawTransactionValueGroupRepository.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/RawTransactionValueGroupRepository.java index 459ee4c..6797b60 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/RawTransactionValueGroupRepository.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/RawTransactionValueGroupRepository.java @@ -1,9 +1,27 @@ package dev.mvvasilev.finances.persistence; import dev.mvvasilev.finances.entity.RawTransactionValueGroup; +import dev.mvvasilev.finances.persistence.dtos.RawTransactionValueGroupDTO; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.Collection; + @Repository public interface RawTransactionValueGroupRepository extends JpaRepository { + + @Query(value = + "SELECT " + + "rtvg.id, " + + "rtvg.name, " + + "rtvg.type " + + "FROM transactions.raw_transaction_value_group AS rtvg " + + "JOIN transactions.raw_statement AS rs ON rtvg.statement_id = rs.id " + + "WHERE rs.user_id = :userId AND rs.id = :statementId", + nativeQuery = true + ) + Collection fetchAllForStatementAndUser(@Param("statementId") Long statementId, @Param("userId") Integer userId); + } diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/dtos/RawStatementDTO.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/dtos/RawStatementDTO.java new file mode 100644 index 0000000..3f33c52 --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/dtos/RawStatementDTO.java @@ -0,0 +1,13 @@ +package dev.mvvasilev.finances.persistence.dtos; + +import java.time.LocalDateTime; + +public interface RawStatementDTO { + + Long getId(); + + String getName(); + + LocalDateTime getTimeCreated(); + +} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/dtos/RawTransactionValueGroupDTO.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/dtos/RawTransactionValueGroupDTO.java new file mode 100644 index 0000000..02bab5b --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/dtos/RawTransactionValueGroupDTO.java @@ -0,0 +1,13 @@ +package dev.mvvasilev.finances.persistence.dtos; + +import dev.mvvasilev.finances.enums.RawTransactionValueType; + +public interface RawTransactionValueGroupDTO { + + Long getId(); + + String getName(); + + int getType(); + +} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/TransactionsService.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatementsService.java similarity index 72% rename from PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/TransactionsService.java rename to PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatementsService.java index 882faee..3c976e9 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/TransactionsService.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatementsService.java @@ -1,5 +1,7 @@ package dev.mvvasilev.finances.services; +import dev.mvvasilev.finances.dtos.TransactionValueGroupDTO; +import dev.mvvasilev.finances.dtos.UploadedStatementDTO; import dev.mvvasilev.finances.entity.RawStatement; import dev.mvvasilev.finances.entity.RawTransactionValue; import dev.mvvasilev.finances.entity.RawTransactionValueGroup; @@ -8,23 +10,14 @@ import dev.mvvasilev.finances.persistence.RawStatementRepository; import dev.mvvasilev.finances.persistence.RawTransactionValueGroupRepository; import dev.mvvasilev.finances.persistence.RawTransactionValueRepository; import jakarta.transaction.Transactional; -import org.apache.commons.collections4.ListUtils; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.math.NumberUtils; -import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.poi.ss.usermodel.CellType; -import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.WorkbookFactory; -import org.apache.poi.ss.util.CellUtil; -import org.hibernate.type.descriptor.DateTimeUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; import java.io.InputStream; -import java.text.ParseException; -import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; @@ -32,13 +25,14 @@ import java.time.format.DateTimeParseException; import java.time.format.ResolverStyle; import java.time.temporal.ChronoField; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Optional; -import java.util.stream.IntStream; +import java.util.stream.Collectors; @Service @Transactional -public class TransactionsService { +public class StatementsService { private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder() .appendPattern("dd.MM.yyyy[ [HH][:mm][:ss]]") @@ -55,14 +49,15 @@ public class TransactionsService { private RawTransactionValueRepository rawTransactionValueRepository; @Autowired - public TransactionsService(RawStatementRepository rawStatementRepository, RawTransactionValueGroupRepository rawTransactionValueGroupRepository, RawTransactionValueRepository rawTransactionValueRepository) { + public StatementsService(RawStatementRepository rawStatementRepository, RawTransactionValueGroupRepository rawTransactionValueGroupRepository, RawTransactionValueRepository rawTransactionValueRepository) { this.rawStatementRepository = rawStatementRepository; this.rawTransactionValueGroupRepository = rawTransactionValueGroupRepository; this.rawTransactionValueRepository = rawTransactionValueRepository; } - public void uploadMultipleTransactionsFromExcelSheetForUser(InputStream workbookInputStream, String userId) throws IOException { + public void uploadStatementFromExcelSheetForUser(String fileName, String mimeType, InputStream workbookInputStream, int userId) throws IOException { + var workbook = WorkbookFactory.create(workbookInputStream); var firstWorksheet = workbook.getSheetAt(0); @@ -70,7 +65,8 @@ public class TransactionsService { var lastRowIndex = firstWorksheet.getLastRowNum(); var statement = new RawStatement(); - statement.setUserId(Integer.parseInt(userId)); + statement.setUserId(userId); + statement.setName(fileName); statement = rawStatementRepository.saveAndFlush(statement); @@ -78,6 +74,7 @@ public class TransactionsService { List valueGroups = new ArrayList<>(); + // turn each column into a value group for (var c : firstRow) { if (c == null || c.getCellType() == CellType.BLANK) { @@ -92,9 +89,11 @@ public class TransactionsService { // group type is string by default, if no other type could have been determined var groupType = RawTransactionValueType.STRING; + // iterate down through the rows on this column, looking for the first one to return a type for (int y = c.getRowIndex() + 1; y <= lastRowIndex; y++) { var typeResult = determineGroupType(firstWorksheet, y, c.getColumnIndex()); + // if a type has been determined, stop here if (typeResult.isPresent()) { groupType = typeResult.get(); break; @@ -109,6 +108,8 @@ public class TransactionsService { valueGroups = rawTransactionValueGroupRepository.saveAllAndFlush(valueGroups); var column = 0; + + // turn each cell in each row into a value, related to the value group ( column ) for (var group : valueGroups) { var valueList = new ArrayList(); @@ -116,6 +117,7 @@ public class TransactionsService { var value = new RawTransactionValue(); value.setGroupId(group.getId()); + value.setRowIndex(y); switch (group.getType()) { case STRING -> value.setStringValue(firstWorksheet.getRow(y).getCell(column).getStringCellValue()); @@ -157,13 +159,31 @@ public class TransactionsService { return Optional.empty(); } - private boolean isValidDate(String inDate) { + private boolean isValidDate(String stringDate) { try { - DATE_FORMAT.parse(inDate.trim()); + DATE_FORMAT.parse(stringDate); } catch (DateTimeParseException e) { return false; } return true; } + public Collection fetchStatementsForUser(int userId) { + return rawStatementRepository.fetchAllForUser(userId) + .stream() + .map(dto -> new UploadedStatementDTO(dto.getId(), dto.getName(), dto.getTimeCreated())) + .collect(Collectors.toList()); + } + + public Collection fetchTransactionValueGroupsForUserStatement(Long statementId, int userId) { + return rawTransactionValueGroupRepository.fetchAllForStatementAndUser(statementId, userId) + .stream() + .map(dto -> new TransactionValueGroupDTO(dto.getId(), dto.getName(), RawTransactionValueType.values()[dto.getType()])) + .collect(Collectors.toList()); + } + + public void deleteStatement(Long statementId, int userId) { + rawStatementRepository.deleteById(statementId); + rawStatementRepository.flush(); + } } diff --git a/PersonalFinancesService/src/main/resources/db/migration/V1.3__AddTransactionCategory.sql b/PersonalFinancesService/src/main/resources/db/migration/V1.3__AddTransactionCategory.sql new file mode 100644 index 0000000..8f47e2b --- /dev/null +++ b/PersonalFinancesService/src/main/resources/db/migration/V1.3__AddTransactionCategory.sql @@ -0,0 +1,9 @@ +CREATE SCHEMA IF NOT EXISTS categories; + +CREATE TABLE IF NOT EXISTS categories.transaction_category ( + id BIGSERIAL, + name VARCHAR(255), + time_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + time_last_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT PK_transaction_category PRIMARY KEY (id) +) \ No newline at end of file diff --git a/PersonalFinancesService/src/main/resources/db/migration/V1.4__AddProcessedTransaction.sql b/PersonalFinancesService/src/main/resources/db/migration/V1.4__AddProcessedTransaction.sql new file mode 100644 index 0000000..c63d232 --- /dev/null +++ b/PersonalFinancesService/src/main/resources/db/migration/V1.4__AddProcessedTransaction.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS transactions.processed_transaction ( + id BIGSERIAL, + description VARCHAR(1024), + amount FLOAT, + is_inflow BOOLEAN, + category_id BIGINT, + timestamp TIMESTAMP, + time_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + time_last_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT PK_processed_transaction PRIMARY KEY (id) +); \ No newline at end of file diff --git a/PersonalFinancesService/src/main/resources/db/migration/V1.5__AddUserIdToTransactionCategory.sql b/PersonalFinancesService/src/main/resources/db/migration/V1.5__AddUserIdToTransactionCategory.sql new file mode 100644 index 0000000..34e81d3 --- /dev/null +++ b/PersonalFinancesService/src/main/resources/db/migration/V1.5__AddUserIdToTransactionCategory.sql @@ -0,0 +1,3 @@ +ALTER TABLE transactions.processed_transaction ADD user_id INTEGER; + +ALTER TABLE categories.transaction_category ADD user_id INTEGER; \ No newline at end of file diff --git a/PersonalFinancesService/src/main/resources/db/migration/V1.6__AddRowIndexToRawTransactionValue.sql b/PersonalFinancesService/src/main/resources/db/migration/V1.6__AddRowIndexToRawTransactionValue.sql new file mode 100644 index 0000000..8526e97 --- /dev/null +++ b/PersonalFinancesService/src/main/resources/db/migration/V1.6__AddRowIndexToRawTransactionValue.sql @@ -0,0 +1 @@ +ALTER TABLE transactions.raw_transaction_value ADD row_index INTEGER NOT NULL; \ No newline at end of file diff --git a/PersonalFinancesService/src/main/resources/db/migration/V1.7__AddNameToRawStatement.sql b/PersonalFinancesService/src/main/resources/db/migration/V1.7__AddNameToRawStatement.sql new file mode 100644 index 0000000..9fcb1c0 --- /dev/null +++ b/PersonalFinancesService/src/main/resources/db/migration/V1.7__AddNameToRawStatement.sql @@ -0,0 +1 @@ +ALTER TABLE transactions.raw_statement ADD name VARCHAR(255); \ No newline at end of file diff --git a/PersonalFinancesService/src/main/resources/db/migration/V1.8__AddOnDeleteCascadeRawStatement.sql b/PersonalFinancesService/src/main/resources/db/migration/V1.8__AddOnDeleteCascadeRawStatement.sql new file mode 100644 index 0000000..7397c03 --- /dev/null +++ b/PersonalFinancesService/src/main/resources/db/migration/V1.8__AddOnDeleteCascadeRawStatement.sql @@ -0,0 +1,11 @@ +ALTER TABLE transactions.raw_transaction_value +DROP CONSTRAINT fk_raw_transaction_value_raw_transaction_value_group; + +ALTER TABLE transactions.raw_transaction_value +ADD CONSTRAINT FK_raw_transaction_value_raw_transaction_value_group FOREIGN KEY (group_id) REFERENCES transactions.raw_transaction_value_group(id) ON DELETE CASCADE; + +ALTER TABLE transactions.raw_transaction_value_group +DROP CONSTRAINT fk_raw_transaction_value_group_raw_statement; + +ALTER TABLE transactions.raw_transaction_value_group +ADD CONSTRAINT FK_raw_transaction_value_group_raw_statement FOREIGN KEY (statement_id) REFERENCES transactions.raw_statement(id) ON DELETE CASCADE; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a09eae6..372e659 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", + "react-material-ui-carousel": "^3.4.2", "react-router-dom": "^6.21.0", "vis-data": "^7.1.9", "vis-network": "^9.1.9" @@ -2722,6 +2723,48 @@ "is-callable": "^1.1.3" } }, + "node_modules/framer-motion": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-4.1.17.tgz", + "integrity": "sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==", + "dependencies": { + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "popmotion": "9.3.6", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": ">=16.8 || ^17.0.0", + "react-dom": ">=16.8 || ^17.0.0" + } + }, + "node_modules/framer-motion/node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/framer-motion/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, + "node_modules/framesync": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-5.3.0.tgz", + "integrity": "sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2978,6 +3021,11 @@ "node": ">= 0.4" } }, + "node_modules/hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -3819,6 +3867,17 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "node_modules/popmotion": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-9.3.6.tgz", + "integrity": "sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==", + "dependencies": { + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + } + }, "node_modules/postcss": { "version": "8.4.32", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", @@ -3938,6 +3997,28 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-material-ui-carousel": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/react-material-ui-carousel/-/react-material-ui-carousel-3.4.2.tgz", + "integrity": "sha512-jUbC5aBWqbbbUOOdUe3zTVf4kMiZFwKJqwhxzHgBfklaXQbSopis4iWAHvEOLcZtSIJk4JAGxKE0CmxDoxvUuw==", + "dependencies": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@mui/icons-material": "^5.4.1", + "@mui/material": "^5.4.1", + "@mui/system": "^5.4.1", + "framer-motion": "^4.1.17" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "@mui/icons-material": "^5.0.0", + "@mui/material": "^5.0.0", + "@mui/system": "^5.0.0", + "react": "^17.0.1 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -4354,6 +4435,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-value-types": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz", + "integrity": "sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==", + "dependencies": { + "hey-listen": "^1.0.8", + "tslib": "^2.1.0" + } + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -4395,6 +4485,11 @@ "node": ">=4" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index f9b4ce8..a944caa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", + "react-material-ui-carousel": "^3.4.2", "react-router-dom": "^6.21.0", "vis-data": "^7.1.9", "vis-network": "^9.1.9" diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1896dcb..7681c67 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,7 +1,7 @@ import { Routes, Route } from 'react-router-dom'; import HomePage from "@/app/pages/HomePage" import RootLayout from '@/app/Layout'; -import TransactionsPage from './app/pages/TransactionsPage'; +import StatementsPage from './app/pages/StatementsPage.jsx'; function App() { return ( @@ -9,7 +9,7 @@ function App() { } /> - } /> + } /> diff --git a/frontend/src/app/Layout.jsx b/frontend/src/app/Layout.jsx index b48fbe6..230bd11 100644 --- a/frontend/src/app/Layout.jsx +++ b/frontend/src/app/Layout.jsx @@ -1,7 +1,7 @@ import * as React from 'react'; import {Link} from 'react-router-dom'; import CssBaseline from '@mui/material/CssBaseline' -import {ThemeProvider, createTheme} from '@mui/material'; +import {ThemeProvider, createTheme, Container, Backdrop} from '@mui/material'; import Box from '@mui/material/Box'; import Drawer from '@mui/material/Drawer'; import Divider from '@mui/material/Divider'; @@ -17,12 +17,13 @@ import {Login as LoginIcon} from "@mui/icons-material"; import {Toaster} from 'react-hot-toast'; import theme from '../components/ThemeRegistry/theme'; import Button from "@mui/material/Button"; +import Grid from "@mui/material/Unstable_Grid2"; const DRAWER_WIDTH = 240; const NAV_LINKS = [ {text: 'Home', to: '/', icon: HomeIcon}, - {text: 'Transactions', to: '/transactions', icon: TransactionsIcon}, + {text: 'Statements', to: '/statements', icon: TransactionsIcon}, ]; const BOTTOM_LINKS = [ @@ -49,6 +50,7 @@ export default function RootLayout({children}) { return ( + {children} + - + ); } diff --git a/frontend/src/app/pages/HomePage.jsx b/frontend/src/app/pages/HomePage.jsx index 475ed10..4b7d4f9 100644 --- a/frontend/src/app/pages/HomePage.jsx +++ b/frontend/src/app/pages/HomePage.jsx @@ -1,7 +1,5 @@ import * as React from 'react'; import Grid from '@mui/material/Unstable_Grid2'; -import Alert from '@mui/material/Alert'; -import AlertTitle from '@mui/material/AlertTitle'; import MediaCard from '@/components/MediaCard'; import { Stack } from '@mui/material'; @@ -9,10 +7,6 @@ export default function HomePage() { return (
- {/* - Hello - This app uses React Router and Material UI v5. - */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/frontend/src/app/pages/StatementsPage.jsx b/frontend/src/app/pages/StatementsPage.jsx new file mode 100644 index 0000000..e5608aa --- /dev/null +++ b/frontend/src/app/pages/StatementsPage.jsx @@ -0,0 +1,156 @@ +import Button from "@mui/material/Button"; +import Box from "@mui/material/Box"; +import { CloudUpload as CloudUploadIcon } from "@mui/icons-material"; +import VisuallyHiddenInput from "@/components/VisuallyHiddenInput.jsx"; +import toast from "react-hot-toast"; +import utils from "@/utils.js"; +import Grid from "@mui/material/Unstable_Grid2"; +import {useEffect, useState} from "react"; +import {Stack} from "@mui/material"; +import VisNetwork from "@/components/statements/VisNetwork.jsx"; +import StatementCard from "@/components/statements/StatementCard.jsx"; +import Carousel from "react-material-ui-carousel"; +import NodeModal from "@/components/statements/NodeModal.jsx"; +import StatementMappingEditor from "@/components/statements/StatementMappingEditor.jsx"; + + +export default function StatementsPage() { + + const [statements, setStatements] = useState([]); + + const [valueGroups, setValueGroups] = useState([]); + + useEffect(() => { + utils.performRequest("/api/statements") + .then(resp => resp.json()) + .then(({ result }) => setStatements(result)); + }, []); + + function fetchStatements() { + utils.performRequest("/api/statements") + .then(resp => resp.json()) + .then(({ result }) => setStatements(result)); + } + + async function uploadStatement({ target }) { + let file = target.files[0]; + + let formData = new FormData(); + formData.append("file", file); + + await toast.promise( + utils.performRequest("/api/statements/uploadSheet", { + method: "POST", + body: formData + }), + { + loading: "Uploading...", + success: () => { + fetchStatements(); + + return "Upload successful!"; + }, + error: (err) => `Uh oh, something went wrong: ${err}` + } + ); + } + + async function mapStatement(e, statementId) { + await toast.promise( + utils.performRequest(`/api/statements/${statementId}/transactionValueGroups`) + .then(res => res.json()) + .then(json => setValueGroups(json.result)), + { + loading: "Preparing...", + success: "Ready", + error: (err) => `Uh oh, something went wrong: ${err}` + } + ); + } + + async function deleteStatement(e, statementId) { + await toast.promise( + utils.performRequest(`/api/statements/${statementId}`, { method: "DELETE" }), + { + loading: "Deleting...", + success: () => { + fetchStatements(); + + return "Success"; + }, + error: (err) => `Uh oh, something went wrong: ${err}` + } + ) + } + + function createCarouselItems() { + let carouselItemCount = Math.ceil(statements.length / 4) || 1; + + let carouselItems = []; + + for (let i = 0; i < carouselItemCount; i++) { + let firstIndex = i * 4; + let lastIndex = firstIndex + 4; + + if (lastIndex > statements.length) { + lastIndex = statements.length; + } + + carouselItems.push( + + { + statements.slice(firstIndex, lastIndex).map((statement) => ( + + + + )) + } + + ); + } + + return carouselItems; + } + + var i = 0; + + return ( + +
+ + + + + + + + + + {createCarouselItems()} + + + + + + + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/pages/TransactionsPage.jsx b/frontend/src/app/pages/TransactionsPage.jsx deleted file mode 100644 index 9dee2c8..0000000 --- a/frontend/src/app/pages/TransactionsPage.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import Button from "@mui/material/Button"; -import { Publish as ImportExportIcon } from "@mui/icons-material"; - -export default function TransactionsPage() { - return ( - <> - - - ); -} \ No newline at end of file diff --git a/frontend/src/components/VisNetwork.jsx b/frontend/src/components/VisNetwork.jsx deleted file mode 100644 index fd638a5..0000000 --- a/frontend/src/components/VisNetwork.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useRef, useEffect } from "react"; -import { Network } from "vis-network"; - -export default function VisNetwork(props) { - const visJsRef = useRef(null); - - const nodes = props.nodes || []; - const edges = props.edges || []; - const options = props.options || {}; - - useEffect(() => { - const network = visJsRef.current && new Network(visJsRef.current, { nodes, edges }, options); - }, [visJsRef, nodes, edges]); - - return
; -} \ No newline at end of file diff --git a/frontend/src/components/VisuallyHiddenInput.jsx b/frontend/src/components/VisuallyHiddenInput.jsx new file mode 100644 index 0000000..1ba48cc --- /dev/null +++ b/frontend/src/components/VisuallyHiddenInput.jsx @@ -0,0 +1,15 @@ +import {styled} from "@mui/material"; + +const VisuallyHiddenInput = styled('input')({ + clip: 'rect(0 0 0 0)', + clipPath: 'inset(50%)', + height: 1, + overflow: 'hidden', + position: 'absolute', + bottom: 0, + left: 0, + whiteSpace: 'nowrap', + width: 1, +}); + +export default VisuallyHiddenInput; \ No newline at end of file diff --git a/frontend/src/components/statements/NodeModal.jsx b/frontend/src/components/statements/NodeModal.jsx new file mode 100644 index 0000000..0c74d6c --- /dev/null +++ b/frontend/src/components/statements/NodeModal.jsx @@ -0,0 +1,30 @@ +import {Modal} from "@mui/material"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; + +export default function NodeModal({ open }) { + return ( + + +

Text in a child modal

+

+ Lorem ipsum, dolor sit amet consectetur adipisicing elit. +

+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/statements/StatementCard.jsx b/frontend/src/components/statements/StatementCard.jsx new file mode 100644 index 0000000..65ec263 --- /dev/null +++ b/frontend/src/components/statements/StatementCard.jsx @@ -0,0 +1,29 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import * as React from "react"; +import CardActions from "@mui/material/CardActions"; +import Button from "@mui/material/Button"; +import { AccountTree as AccountTreeIcon } from "@mui/icons-material"; +import { Delete as DeleteIcon } from "@mui/icons-material"; +import Grid from "@mui/material/Unstable_Grid2"; + +export default function StatementCard({ name, timeUploaded, id, onMap, onDelete }) { + return ( + + + + {name} uploaded on {new Date(timeUploaded).toLocaleString("en-GB")} + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/statements/StatementMappingEditor.jsx b/frontend/src/components/statements/StatementMappingEditor.jsx new file mode 100644 index 0000000..36e9571 --- /dev/null +++ b/frontend/src/components/statements/StatementMappingEditor.jsx @@ -0,0 +1,42 @@ +import VisNetwork from "@/components/statements/VisNetwork.jsx"; +import Grid from "@mui/material/Unstable_Grid2"; +import Button from "@mui/material/Button"; +import NodeModal from "@/components/statements/NodeModal.jsx"; + +export default function StatementMappingEditor({ shown: shown = false, valueGroups: valueGroups = [] }) { + + + var i = 0; + + return ( + + + { + return { + id: group.id, + label: group.name, + x: 0, y: (i++) * 10 + } + })} + backgroundColor="#222222" + options={{ + height: "1000px" + }} + /> + + + + + + + + + + + + {/**/} + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/statements/VisNetwork.jsx b/frontend/src/components/statements/VisNetwork.jsx new file mode 100644 index 0000000..c4b5025 --- /dev/null +++ b/frontend/src/components/statements/VisNetwork.jsx @@ -0,0 +1,29 @@ +import {useRef, useEffect, useState} from "react"; +import { Network } from "vis-network"; + +export default function VisNetwork({ nodes: nodes = [], edges: edges = [], backgroundColor: backgroundColor = "#ffffff", options: options = {} }) { + const visJsRef = useRef(null); + const [network, setNetwork] = useState(); + + useEffect(() => { + const network = visJsRef.current && new Network(visJsRef.current, { nodes, edges }, options); + + network.on("beforeDrawing", function(ctx) { + // save current translate/zoom + ctx.save(); + + // reset transform to identity + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = backgroundColor; + + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height) + + // restore old transform + ctx.restore(); + }) + + }, [visJsRef, backgroundColor, edges, nodes, options]); + + return
; +} \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 3257435..feff02a 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -5,9 +5,7 @@ import App from './App.jsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( - - , ) diff --git a/frontend/src/utils.js b/frontend/src/utils.js new file mode 100644 index 0000000..ea1a5cf --- /dev/null +++ b/frontend/src/utils.js @@ -0,0 +1,19 @@ +let utils = { + performRequest: async (url, options) => { + return await fetch(url, options).then(resp => { + if (resp.status === 401) { + window.location.replace("https://localhost:8080/oauth2/authorization/authentik") + + throw "Unauthorized, please login."; + } + + if (!resp.ok) { + throw resp.status; + } + + return resp; + }); + } +} + +export default utils; \ No newline at end of file