mirror of
https://github.com/mvvasilev/personal-finances.git
synced 2025-05-08 23:29:52 +03:00
Add statements. Add transactions. Add processed transactions. Add visjs react component.
This commit is contained in:
parent
6e190d09f0
commit
4748e388e0
41 changed files with 801 additions and 296 deletions
|
@ -1,4 +0,0 @@
|
|||
package dev.mvvasilev.common.data;
|
||||
|
||||
public class DataNamingStrategy {
|
||||
}
|
|
@ -21,7 +21,7 @@ public class APIResponseAdvice {
|
|||
private Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public APIResponseDTO<Object> processGenericException(Exception ex) {
|
||||
public ResponseEntity<APIResponseDTO<Object>> processGenericException(Exception ex) {
|
||||
List<APIErrorDTO> errors = List.of(
|
||||
new APIErrorDTO(
|
||||
ex.getMessage(),
|
||||
|
@ -32,11 +32,12 @@ public class APIResponseAdvice {
|
|||
|
||||
logger.error("Exception", ex);
|
||||
|
||||
return new APIResponseDTO<>(
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new APIResponseDTO<>(
|
||||
null,
|
||||
errors,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<APIResponseDTO<Collection<UploadedStatementDTO>>> fetchStatements(Authentication authentication) {
|
||||
return ResponseEntity.ofNullable(
|
||||
ok(statementsService.fetchStatementsForUser(Integer.parseInt(authentication.getName())))
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/{statementId}/transactionValueGroups")
|
||||
public ResponseEntity<APIResponseDTO<Collection<TransactionValueGroupDTO>>> fetchTransactionValueGroups(@PathVariable("statementId") Long statementId, Authentication authentication) {
|
||||
return ResponseEntity.ofNullable(ok(
|
||||
statementsService.fetchTransactionValueGroupsForUserStatement(statementId, Integer.parseInt(authentication.getName()))
|
||||
));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{statementId}")
|
||||
public ResponseEntity<APIResponseDTO> 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<APIResponseDTO<Integer>> 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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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<APIResponseDTO<Integer>> uploadTransactions(@RequestParam("file") MultipartFile file, Authentication authentication) throws IOException {
|
||||
transactionsService.uploadMultipleTransactionsFromExcelSheetForUser(
|
||||
file.getInputStream(),
|
||||
authentication.getName()
|
||||
);
|
||||
return ResponseEntity.ofNullable(ok(1));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package dev.mvvasilev.finances.dtos;
|
||||
|
||||
import dev.mvvasilev.finances.enums.RawTransactionValueType;
|
||||
|
||||
public record TransactionValueGroupDTO(
|
||||
Long id,
|
||||
String name,
|
||||
RawTransactionValueType type
|
||||
) {}
|
|
@ -0,0 +1,9 @@
|
|||
package dev.mvvasilev.finances.dtos;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record UploadedStatementDTO (
|
||||
Long id,
|
||||
String name,
|
||||
LocalDateTime timeUploaded
|
||||
) {}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
// }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
//
|
||||
//
|
||||
//
|
||||
//}
|
|
@ -1,7 +0,0 @@
|
|||
package dev.mvvasilev.finances.enums;
|
||||
|
||||
public enum TransactionCategory {
|
||||
Food,
|
||||
Shopping,
|
||||
Miscellaneous
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package dev.mvvasilev.finances.enums;
|
||||
|
||||
public enum TransactionType {
|
||||
|
||||
INFLOW,
|
||||
OUTFLOW
|
||||
|
||||
}
|
|
@ -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<RawStatement, Long> {
|
||||
|
||||
@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<RawStatementDTO> fetchAllForUser(@Param("userId") int userId);
|
||||
|
||||
}
|
||||
|
|
|
@ -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<RawTransactionValueGroup, Long> {
|
||||
|
||||
@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<RawTransactionValueGroupDTO> fetchAllForStatementAndUser(@Param("statementId") Long statementId, @Param("userId") Integer userId);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package dev.mvvasilev.finances.persistence.dtos;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public interface RawStatementDTO {
|
||||
|
||||
Long getId();
|
||||
|
||||
String getName();
|
||||
|
||||
LocalDateTime getTimeCreated();
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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<RawTransactionValueGroup> 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<RawTransactionValue>();
|
||||
|
||||
|
@ -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<UploadedStatementDTO> fetchStatementsForUser(int userId) {
|
||||
return rawStatementRepository.fetchAllForUser(userId)
|
||||
.stream()
|
||||
.map(dto -> new UploadedStatementDTO(dto.getId(), dto.getName(), dto.getTimeCreated()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Collection<TransactionValueGroupDTO> 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();
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
|
@ -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)
|
||||
);
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE transactions.processed_transaction ADD user_id INTEGER;
|
||||
|
||||
ALTER TABLE categories.transaction_category ADD user_id INTEGER;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE transactions.raw_transaction_value ADD row_index INTEGER NOT NULL;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE transactions.raw_statement ADD name VARCHAR(255);
|
|
@ -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;
|
95
frontend/package-lock.json
generated
95
frontend/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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() {
|
|||
<RootLayout>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/transactions" element={<TransactionsPage />} />
|
||||
<Route path="/statements" element={<StatementsPage />} />
|
||||
</Routes>
|
||||
</RootLayout>
|
||||
</>
|
||||
|
|
|
@ -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 (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline/>
|
||||
|
||||
<Drawer
|
||||
sx={{
|
||||
width: DRAWER_WIDTH,
|
||||
|
@ -112,12 +114,14 @@ export default function RootLayout({children}) {
|
|||
top: 0,
|
||||
flexGrow: 1,
|
||||
bgcolor: 'background.default',
|
||||
ml: `${DRAWER_WIDTH}px`,
|
||||
left: `${DRAWER_WIDTH}px`,
|
||||
right: 0,
|
||||
p: 3,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
<Toaster
|
||||
toastOptions={{
|
||||
success: {
|
||||
|
|
|
@ -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 (
|
||||
<Stack>
|
||||
<div>
|
||||
{/* <Alert severity="info" sx={{ mt: 2, mb: 5 }}>
|
||||
<AlertTitle>Hello</AlertTitle>
|
||||
This app uses React Router and Material UI v5.
|
||||
</Alert> */}
|
||||
<Grid container rowSpacing={3} columnSpacing={3}>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
|
@ -32,108 +26,6 @@ export default function HomePage() {
|
|||
text="An RGB color space is any additive color space based on the RGB color model. RGB color spaces are commonly found describing the input signal to display devices such as television screens and computer monitors."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="CIELAB"
|
||||
text="The CIELAB color space, also referred to as L*a*b*, was intended as a perceptually uniform space, where a given numerical change corresponds to a similar perceived change in color."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="CMYK"
|
||||
text="The CMYK color model (also known as process color, or four color) is a subtractive color model, based on the CMY color model, used in color printing, and is also used to describe the printing process itself."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="HSL and HSV"
|
||||
text="HSL (for hue, saturation, lightness) and HSV (for hue, saturation, value; also known as HSB, for hue, saturation, brightness) are alternative representations of the RGB color model, designed in the 1970s by computer graphics researchers."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="RGB"
|
||||
text="An RGB color space is any additive color space based on the RGB color model. RGB color spaces are commonly found describing the input signal to display devices such as television screens and computer monitors."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="CIELAB"
|
||||
text="The CIELAB color space, also referred to as L*a*b*, was intended as a perceptually uniform space, where a given numerical change corresponds to a similar perceived change in color."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="CMYK"
|
||||
text="The CMYK color model (also known as process color, or four color) is a subtractive color model, based on the CMY color model, used in color printing, and is also used to describe the printing process itself."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="HSL and HSV"
|
||||
text="HSL (for hue, saturation, lightness) and HSV (for hue, saturation, value; also known as HSB, for hue, saturation, brightness) are alternative representations of the RGB color model, designed in the 1970s by computer graphics researchers."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="RGB"
|
||||
text="An RGB color space is any additive color space based on the RGB color model. RGB color spaces are commonly found describing the input signal to display devices such as television screens and computer monitors."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="CIELAB"
|
||||
text="The CIELAB color space, also referred to as L*a*b*, was intended as a perceptually uniform space, where a given numerical change corresponds to a similar perceived change in color."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="CMYK"
|
||||
text="The CMYK color model (also known as process color, or four color) is a subtractive color model, based on the CMY color model, used in color printing, and is also used to describe the printing process itself."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="HSL and HSV"
|
||||
text="HSL (for hue, saturation, lightness) and HSV (for hue, saturation, value; also known as HSB, for hue, saturation, brightness) are alternative representations of the RGB color model, designed in the 1970s by computer graphics researchers."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="RGB"
|
||||
text="An RGB color space is any additive color space based on the RGB color model. RGB color spaces are commonly found describing the input signal to display devices such as television screens and computer monitors."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="CIELAB"
|
||||
text="The CIELAB color space, also referred to as L*a*b*, was intended as a perceptually uniform space, where a given numerical change corresponds to a similar perceived change in color."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="CMYK"
|
||||
text="The CMYK color model (also known as process color, or four color) is a subtractive color model, based on the CMY color model, used in color printing, and is also used to describe the printing process itself."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="HSL and HSV"
|
||||
text="HSL (for hue, saturation, lightness) and HSV (for hue, saturation, value; also known as HSB, for hue, saturation, brightness) are alternative representations of the RGB color model, designed in the 1970s by computer graphics researchers."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="RGB"
|
||||
text="An RGB color space is any additive color space based on the RGB color model. RGB color spaces are commonly found describing the input signal to display devices such as television screens and computer monitors."
|
||||
/>
|
||||
</Grid>
|
||||
<Grid xs={4}>
|
||||
<MediaCard
|
||||
heading="CIELAB"
|
||||
text="The CIELAB color space, also referred to as L*a*b*, was intended as a perceptually uniform space, where a given numerical change corresponds to a similar perceived change in color."
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</Stack>
|
||||
|
|
156
frontend/src/app/pages/StatementsPage.jsx
Normal file
156
frontend/src/app/pages/StatementsPage.jsx
Normal file
|
@ -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(
|
||||
<Grid key={i} container spacing={2}>
|
||||
{
|
||||
statements.slice(firstIndex, lastIndex).map((statement) => (
|
||||
<Grid key={statement.id} xs={3}>
|
||||
<StatementCard
|
||||
id={statement.id}
|
||||
timeUploaded={statement.timeUploaded}
|
||||
name={statement.name}
|
||||
onMap={mapStatement}
|
||||
onDelete={deleteStatement}
|
||||
/>
|
||||
</Grid>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return carouselItems;
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<div>
|
||||
<Grid container rowSpacing={3} columnSpacing={3}>
|
||||
<Grid xs={3}>
|
||||
<Button component="label" variant="contained" startIcon={<CloudUploadIcon />}>
|
||||
Upload Statement
|
||||
<VisuallyHiddenInput type="file" onChange={uploadStatement} />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid xs={9}></Grid>
|
||||
|
||||
<Grid xs={12}>
|
||||
<Carousel
|
||||
cycleNavigation
|
||||
fullHeightHover
|
||||
swipe
|
||||
animation={"slide"}
|
||||
duration={100}
|
||||
autoPlay={false}
|
||||
>
|
||||
{createCarouselItems()}
|
||||
</Carousel>
|
||||
</Grid>
|
||||
|
||||
<Grid xs={12}>
|
||||
<StatementMappingEditor valueGroups={valueGroups} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import Button from "@mui/material/Button";
|
||||
import { Publish as ImportExportIcon } from "@mui/icons-material";
|
||||
|
||||
export default function TransactionsPage() {
|
||||
return (
|
||||
<>
|
||||
<Button variant="contained" onClick={() => {
|
||||
fetch("/api/user-info", {
|
||||
method: "POST"
|
||||
})
|
||||
.then((resp) => {
|
||||
console.log(resp)
|
||||
});
|
||||
}}>
|
||||
<ImportExportIcon /> Import
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 <div ref={visJsRef} />;
|
||||
}
|
15
frontend/src/components/VisuallyHiddenInput.jsx
Normal file
15
frontend/src/components/VisuallyHiddenInput.jsx
Normal file
|
@ -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;
|
30
frontend/src/components/statements/NodeModal.jsx
Normal file
30
frontend/src/components/statements/NodeModal.jsx
Normal file
|
@ -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 (
|
||||
<Modal open={open}>
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
pt: 2,
|
||||
px: 4,
|
||||
pb: 3,
|
||||
width: "400px"
|
||||
}}
|
||||
>
|
||||
<h2 id="child-modal-title">Text in a child modal</h2>
|
||||
<p id="child-modal-description">
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit.
|
||||
</p>
|
||||
<Button>Close Child Modal</Button>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
}
|
29
frontend/src/components/statements/StatementCard.jsx
Normal file
29
frontend/src/components/statements/StatementCard.jsx
Normal file
|
@ -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 (
|
||||
<Card id={`statement-${id}`}>
|
||||
<CardContent>
|
||||
<Typography gutterBottom>
|
||||
{name} uploaded on {new Date(timeUploaded).toLocaleString("en-GB")}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button variant="contained" size="small" onClick={(e) => onMap(e, id)} startIcon={<AccountTreeIcon />}>
|
||||
Map
|
||||
</Button>
|
||||
<Button variant="contained" size="small" onClick={(e) => onDelete(e, id)} startIcon={<DeleteIcon />}>
|
||||
Delete
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<Grid justifyContent={"space-between"} container columnSpacing={3}>
|
||||
<Grid xs={9}>
|
||||
<VisNetwork
|
||||
nodes={valueGroups.map(group => {
|
||||
return {
|
||||
id: group.id,
|
||||
label: group.name,
|
||||
x: 0, y: (i++) * 10
|
||||
}
|
||||
})}
|
||||
backgroundColor="#222222"
|
||||
options={{
|
||||
height: "1000px"
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid xs={3} container columnSpacing={1}>
|
||||
<Grid xs={6}>
|
||||
<Button>Add Node</Button>
|
||||
</Grid>
|
||||
<Grid xs={6}>
|
||||
<Button>Add Connection</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/*<NodeModal open={true}></NodeModal>*/}
|
||||
|
||||
</Grid>
|
||||
);
|
||||
}
|
29
frontend/src/components/statements/VisNetwork.jsx
Normal file
29
frontend/src/components/statements/VisNetwork.jsx
Normal file
|
@ -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 <div ref={visJsRef} />;
|
||||
}
|
|
@ -5,9 +5,7 @@ import App from './App.jsx'
|
|||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
|
19
frontend/src/utils.js
Normal file
19
frontend/src/utils.js
Normal file
|
@ -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;
|
Loading…
Add table
Reference in a new issue