- Refactored transcations table and queries to use catetories table
- Added historical rates chart for investments page
This commit is contained in:
parent
60ddc816a0
commit
f5b3e49ca6
Binary file not shown.
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 162 KiB |
|
|
@ -135,7 +135,8 @@ public class PdfManager {
|
||||||
|
|
||||||
if (matchedCategory != null) {
|
if (matchedCategory != null) {
|
||||||
String label = matchedCategory.replace(" ", "");
|
String label = matchedCategory.replace(" ", "");
|
||||||
transaction.setCategory(matchedCategory);
|
int categoryId = categoriesRepo.findCategoryIdByCategoryName(matchedCategory);
|
||||||
|
transaction.setCategory(Integer.toString(categoryId));
|
||||||
|
|
||||||
int endIndex = rightmostIndex + label.length();
|
int endIndex = rightmostIndex + label.length();
|
||||||
line = line.substring(0, rightmostIndex) + line.substring(endIndex);
|
line = line.substring(0, rightmostIndex) + line.substring(endIndex);
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,7 @@ import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
import com.example.FinanceTracker.Components.BootstrapColours;
|
import com.example.FinanceTracker.Components.BootstrapColours;
|
||||||
// import com.example.FinanceTracker.Components.ColourComponent;
|
import com.example.FinanceTracker.Components.PdfManager;
|
||||||
//import com.example.FinanceTracker.Components.PdfManager;
|
|
||||||
import com.example.FinanceTracker.DTOs.GroupedTransaction;
|
import com.example.FinanceTracker.DTOs.GroupedTransaction;
|
||||||
import com.example.FinanceTracker.DTOs.Home.OverviewCard;
|
import com.example.FinanceTracker.DTOs.Home.OverviewCard;
|
||||||
import com.example.FinanceTracker.Models.Transaction;
|
import com.example.FinanceTracker.Models.Transaction;
|
||||||
|
|
@ -27,8 +26,8 @@ import com.example.FinanceTracker.Services.TransactionService;
|
||||||
@Controller
|
@Controller
|
||||||
public class HomeController {
|
public class HomeController {
|
||||||
|
|
||||||
// @Autowired
|
@Autowired
|
||||||
// private PdfManager pdfManager;
|
private PdfManager pdfManager;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TransactionService transactionsService;
|
private TransactionService transactionsService;
|
||||||
|
|
@ -42,7 +41,7 @@ public class HomeController {
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
public String Home(Model model) {
|
public String Home(Model model) {
|
||||||
|
|
||||||
// transactionsService.saveAll(pdfManager.extractTransactions());
|
//transactionsService.saveAll(pdfManager.extractTransactions());
|
||||||
|
|
||||||
// Balance Text
|
// Balance Text
|
||||||
model.addAttribute("latestBalance",
|
model.addAttribute("latestBalance",
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,52 @@
|
||||||
package com.example.FinanceTracker.Controllers;
|
package com.example.FinanceTracker.Controllers;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.client.RestClient;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import com.example.FinanceTracker.DTOs.Investments.CurrRate;
|
import com.example.FinanceTracker.DTOs.Investments.CurrRate;
|
||||||
import com.example.FinanceTracker.DTOs.Investments.Currency;
|
import com.example.FinanceTracker.DTOs.Investments.Currency;
|
||||||
import com.example.FinanceTracker.DTOs.Investments.ExchangeRate;
|
import com.example.FinanceTracker.DTOs.Investments.ExchangeRate;
|
||||||
|
import com.example.FinanceTracker.Services.InvestmentService;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
public class InvestmentsController {
|
public class InvestmentsController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RestClient restClient;
|
private InvestmentService investmentService;
|
||||||
|
|
||||||
@GetMapping("/investments")
|
@GetMapping("/investments")
|
||||||
public String investments(Model model) {
|
public String investments(Model model) {
|
||||||
|
|
||||||
|
// Create Currency Pills
|
||||||
|
List<CurrRate> allRates = investmentService.getCurrRates();
|
||||||
|
model.addAttribute("currRates", allRates);
|
||||||
|
|
||||||
// Populate Historical Rates Dropdown
|
// Populate Historical Rates Dropdown
|
||||||
List<Currency> allCurrencies = getAllCurrencies();
|
List<Currency> allCurrencies = investmentService.getAllCurrencies();
|
||||||
model.addAttribute("allCurrencies", allCurrencies);
|
model.addAttribute("allCurrencies", allCurrencies);
|
||||||
|
|
||||||
// Create Currency Pills
|
// historical rates chart
|
||||||
List<CurrRate> allRates = getCurrRates();
|
List<ExchangeRate> historicalExchangeRates = investmentService.getHistoricalRates("USD", 1).reversed();
|
||||||
model.addAttribute("currRates", allRates);
|
|
||||||
|
// Get all the rates from the historical Exchange Rates
|
||||||
|
ArrayList<BigDecimal> historicalRates = new ArrayList<>();
|
||||||
|
ArrayList<LocalDate> historicalRatesLabels = new ArrayList<>();
|
||||||
|
for (ExchangeRate exchangeRate : historicalExchangeRates) {
|
||||||
|
historicalRates.add(exchangeRate.getRate());
|
||||||
|
historicalRatesLabels.add(exchangeRate.getCreatedAt().toLocalDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("historicalRates", historicalRates);
|
||||||
|
model.addAttribute("labels", historicalRatesLabels);
|
||||||
|
|
||||||
// footer
|
// footer
|
||||||
model.addAttribute("footerDetails", "Copyright © FinanceTracker " + LocalDate.now().getYear());
|
model.addAttribute("footerDetails", "Copyright © FinanceTracker " + LocalDate.now().getYear());
|
||||||
|
|
@ -35,53 +54,50 @@ public class InvestmentsController {
|
||||||
return "investments";
|
return "investments";
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<CurrRate> getCurrRates() {
|
@GetMapping("/historicalRatesWeekly")
|
||||||
return List.of("EUR", "USD", "JPY", "GBP", "CAD", "AUD").stream()
|
public String showHistoricalRatesWeek(Model model,
|
||||||
.map(code -> {
|
@RequestParam("historicalRatesWeekly") boolean historicalRatesWeekly){
|
||||||
Double rate = restClient.get()
|
|
||||||
.uri(uriBuilder -> uriBuilder
|
|
||||||
.path("/exchangeRate/pair")
|
|
||||||
.queryParam("from", code)
|
|
||||||
.queryParam("to", "ZAR")
|
|
||||||
.build())
|
|
||||||
.retrieve()
|
|
||||||
.body(Double.class);
|
|
||||||
|
|
||||||
return new CurrRate(code, rate.toString(), getChange(code));
|
// historical rates chart
|
||||||
})
|
List<ExchangeRate> historicalExchangeRates = investmentService.getHistoricalRates("USD", 3).reversed();
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getChange(String targetCurr) {
|
// Get all the rates from the historical Exchange Rates
|
||||||
|
ArrayList<BigDecimal> historicalRates = new ArrayList<>();
|
||||||
final String effectiveTargetCurr = targetCurr.toUpperCase().contains("USD") ? "ZAR" : targetCurr;
|
ArrayList<LocalDate> historicalRatesLabels = new ArrayList<>();
|
||||||
|
for (ExchangeRate exchangeRate : historicalExchangeRates) {
|
||||||
List<ExchangeRate> exchangeRate = restClient.get()
|
historicalRates.add(exchangeRate.getRate());
|
||||||
.uri(uriBuilder -> uriBuilder
|
historicalRatesLabels.add(exchangeRate.getCreatedAt().toLocalDate());
|
||||||
.path("exchangeRate/Historical")
|
|
||||||
.queryParam("targetCurrency", effectiveTargetCurr)
|
|
||||||
.queryParam("fromDate", LocalDate.now().minusDays(1))
|
|
||||||
.queryParam("toDate", LocalDate.now().plusDays(1))
|
|
||||||
.build())
|
|
||||||
.retrieve()
|
|
||||||
.body(new ParameterizedTypeReference<List<ExchangeRate>>() {
|
|
||||||
});
|
|
||||||
|
|
||||||
if (exchangeRate.get(0).getRate().compareTo(exchangeRate.get(1).getRate()) > 0) {
|
|
||||||
return "UP";
|
|
||||||
} else {
|
|
||||||
return "DOWN";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model.addAttribute("historicalRates", historicalRates);
|
||||||
|
model.addAttribute("labels", historicalRatesLabels);
|
||||||
|
|
||||||
|
return "investments :: HistoricalRatesChart";
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Currency> getAllCurrencies() {
|
|
||||||
|
|
||||||
return restClient.get()
|
@GetMapping("/changeHistoricalCurr")
|
||||||
.uri("/currency/currencies")
|
public String changeHistoricalCurr(Model model,
|
||||||
.retrieve()
|
@RequestParam("currencyChange") String currencyChange){
|
||||||
.body(new ParameterizedTypeReference<List<Currency>>() {
|
|
||||||
});
|
// historical rates chart
|
||||||
|
List<ExchangeRate> historicalExchangeRates = investmentService.getHistoricalRates(currencyChange, 3).reversed();
|
||||||
|
|
||||||
|
// Get all the rates from the historical Exchange Rates
|
||||||
|
ArrayList<BigDecimal> historicalRates = new ArrayList<>();
|
||||||
|
ArrayList<LocalDate> historicalRatesLabels = new ArrayList<>();
|
||||||
|
for (ExchangeRate exchangeRate : historicalExchangeRates) {
|
||||||
|
historicalRates.add(exchangeRate.getRate());
|
||||||
|
historicalRatesLabels.add(exchangeRate.getCreatedAt().toLocalDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("historicalRates", historicalRates);
|
||||||
|
model.addAttribute("labels", historicalRatesLabels);
|
||||||
|
|
||||||
|
return "investments :: HistoricalRatesChart";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ public class Transaction {
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private String category;
|
private int category;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private BigDecimal moneyIn;
|
private BigDecimal moneyIn;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import java.util.List;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import com.example.FinanceTracker.Models.Categories;
|
import com.example.FinanceTracker.Models.Categories;
|
||||||
|
|
@ -15,4 +16,7 @@ public interface CategoriesRepo extends JpaRepository<Categories, Integer>{
|
||||||
@Query(value = "SELECT category_name FROM categories", nativeQuery=true)
|
@Query(value = "SELECT category_name FROM categories", nativeQuery=true)
|
||||||
List<String> findAllCategoryNames();
|
List<String> findAllCategoryNames();
|
||||||
|
|
||||||
|
@Query(value = "SELECT id FROM categories WHERE category_name = :category_name", nativeQuery = true)
|
||||||
|
int findCategoryIdByCategoryName(@Param("category_name") String category_name);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,162 +18,172 @@ import com.example.FinanceTracker.Models.Transaction;
|
||||||
@Repository
|
@Repository
|
||||||
public interface TransactionRepo extends JpaRepository<Transaction, UUID> {
|
public interface TransactionRepo extends JpaRepository<Transaction, UUID> {
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE date BETWEEN :fromDate AND :toDate
|
WHERE date BETWEEN :fromDate AND :toDate
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
Page<Transaction> getTransactionsBetween(Pageable pageable, @Param("fromDate") LocalDate fromDate,
|
Page<Transaction> getTransactionsBetween(Pageable pageable, @Param("fromDate") LocalDate fromDate,
|
||||||
@Param("toDate") LocalDate toDate);
|
@Param("toDate") LocalDate toDate);
|
||||||
|
|
||||||
@Query(value = "SELECT * FROM transactions ORDER BY date DESC", nativeQuery = true)
|
@Query(value = """
|
||||||
Page<Transaction> findAllPaged(Pageable pageable);
|
SELECT t.balance, t.date, t.money_in, t.money_out,
|
||||||
|
t.description, c.category_name as category, t.fee,
|
||||||
|
t.id
|
||||||
|
FROM transactions t
|
||||||
|
JOIN categories c ON t.category = c.id
|
||||||
|
ORDER BY date DESC
|
||||||
|
""", nativeQuery = true)
|
||||||
|
Page<Transaction> findAllPaged(Pageable pageable);
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT * FROM transactions
|
SELECT
|
||||||
WHERE CONCAT_WS('', date, description, category, money_in, money_out, fee, balance)
|
t.balance, t.date, t.money_in, t.money_out, t.description, c.category_name as category, t.fee, t.id
|
||||||
LIKE Concat('%', :searchValue, '%')
|
FROM transactions t
|
||||||
ORDER BY date
|
JOIN categories c ON t.category = c.id
|
||||||
""", nativeQuery = true)
|
WHERE CONCAT_WS('', t.balance, t.date, t.money_in, t.money_out, t.description, c.category_name, t.fee)
|
||||||
Page<Transaction> searchTransactions(Pageable pageable, @Param("searchValue") String SearchValue);
|
LIKE Concat('%', :searchValue, '%')
|
||||||
|
ORDER BY date
|
||||||
|
""", nativeQuery = true)
|
||||||
|
Page<Transaction> searchTransactions(Pageable pageable, @Param("searchValue") String SearchValue);
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT (balance - 30) as balance
|
SELECT (balance - 30) as balance
|
||||||
FROM transactions ORDER BY date DESC LIMIT 1
|
FROM transactions ORDER BY date DESC LIMIT 1
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double getLatestBalance();
|
double getLatestBalance();
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// MONEY IN QUERIES
|
// MONEY IN QUERIES
|
||||||
// =============================================================
|
// =============================================================
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT SUM(money_in)
|
SELECT SUM(money_in)
|
||||||
FROM transactions
|
FROM transactions
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double getTotalMoneyIn();
|
double getTotalMoneyIn();
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT SUM(money_in)
|
SELECT SUM(money_in)
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE YEAR(date) = :year
|
WHERE YEAR(date) = :year
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double getTotalMoneyInByYear(@Param("year") int year);
|
double getTotalMoneyInByYear(@Param("year") int year);
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT SUM(money_in) / (SELECT COUNT(DISTINCT MONTH(date)) FROM transactions WHERE YEAR(date) = :year)
|
SELECT SUM(money_in) / (SELECT COUNT(DISTINCT MONTH(date)) FROM transactions WHERE YEAR(date) = :year)
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE YEAR(date) = :year
|
WHERE YEAR(date) = :year
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double getAverageMonthlyMoneyIn(int year);
|
double getAverageMonthlyMoneyIn(int year);
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT category, SUM(money_in)
|
SELECT c.category_name, SUM(money_in)
|
||||||
FROM transactions
|
FROM transactions t JOIN categories c ON t.category = c.id
|
||||||
WHERE money_in != 0
|
WHERE money_in != 0
|
||||||
GROUP BY category
|
GROUP BY category
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
List<GroupedTransaction> getIncomeGroupedTransactions();
|
List<GroupedTransaction> getIncomeGroupedTransactions();
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// MONEY OUT QUERIES
|
// MONEY OUT QUERIES
|
||||||
// =============================================================
|
// =============================================================
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT SUM(money_out)
|
SELECT SUM(money_out)
|
||||||
FROM transactions
|
FROM transactions
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double getTotalMoneyOut();
|
double getTotalMoneyOut();
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT SUM(money_out) / (SELECT COUNT(DISTINCT MONTH(date)) FROM transactions WHERE YEAR(date) = :year)
|
SELECT SUM(money_out) / (SELECT COUNT(DISTINCT MONTH(date)) FROM transactions WHERE YEAR(date) = :year)
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE YEAR(date) = :year
|
WHERE YEAR(date) = :year
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double getAverageMonthlyMoneyOut(int year);
|
double getAverageMonthlyMoneyOut(int year);
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT category, SUM(money_out)
|
SELECT c.category_name, SUM(money_out)
|
||||||
FROM transactions
|
FROM transactions t JOIN categories c ON t.category = c.id
|
||||||
WHERE money_out != 0
|
WHERE money_out != 0
|
||||||
GROUP BY category
|
GROUP BY category
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
List<GroupedTransaction> getExpenditureGroupedTransactions();
|
List<GroupedTransaction> getExpenditureGroupedTransactions();
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// FEE QUERIES
|
// FEE QUERIES
|
||||||
// =============================================================
|
// =============================================================
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT SUM(fee)
|
SELECT SUM(fee)
|
||||||
FROM transactions
|
FROM transactions
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double getTotalFees();
|
double getTotalFees();
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT SUM(fee)
|
SELECT SUM(fee)
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE YEAR(date) = :year
|
WHERE YEAR(date) = :year
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double getTotalFeesByYear(@Param("year") int year);
|
double getTotalFeesByYear(@Param("year") int year);
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// INTEREST QUERIES
|
// INTEREST QUERIES
|
||||||
// =============================================================
|
// =============================================================
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT SUM(money_in)
|
SELECT SUM(money_in)
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE category = 'Interest'
|
WHERE category = 'Interest'
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double getTotalInterest();
|
double getTotalInterest();
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT SUM(money_in) / (SELECT COUNT(DISTINCT MONTH(date)) FROM transactions WHERE YEAR(date) = :year)
|
SELECT SUM(money_in) / (SELECT COUNT(DISTINCT MONTH(date)) FROM transactions WHERE YEAR(date) = :year)
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE YEAR(date) = :year AND category = 'Interest'
|
WHERE YEAR(date) = :year AND category = 'Interest'
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double averageMonthlyInterest(@Param("year") int year);
|
double averageMonthlyInterest(@Param("year") int year);
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// OTHER QUERIES
|
// OTHER QUERIES
|
||||||
// =============================================================
|
// =============================================================
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT
|
SELECT
|
||||||
CONCAT('Q', QUARTER(date)) AS Quarters,
|
CONCAT('Q', QUARTER(date)) AS Quarters,
|
||||||
SUM(money_in) AS \"Total MoneyIn\",
|
SUM(money_in) AS \"Total MoneyIn\",
|
||||||
SUM(money_out) AS \"Total MoneyOut\",
|
SUM(money_out) AS \"Total MoneyOut\",
|
||||||
SUM(fee) AS \"Total Fees\"
|
SUM(fee) AS \"Total Fees\"
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE YEAR(date) = :year
|
WHERE YEAR(date) = :year
|
||||||
GROUP BY QUARTER(date)
|
GROUP BY QUARTER(date)
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
List<QuartlyTotals> getQuarterlyTotals(@Param("year") int year);
|
List<QuartlyTotals> getQuarterlyTotals(@Param("year") int year);
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT
|
SELECT
|
||||||
(SELECT SUM(money_in) FROM transactions WHERE category != 'Interest' AND money_in != 0) AS money_in,
|
(SELECT SUM(money_in) FROM transactions WHERE category != 'Interest' AND money_in != 0) AS money_in,
|
||||||
(SELECT SUM(money_out) * -1 FROM transactions WHERE category != 'Fees' AND money_out != 0) AS money_out,
|
(SELECT SUM(money_out) * -1 FROM transactions WHERE category != 'Fees' AND money_out != 0) AS money_out,
|
||||||
(SELECT SUM(money_in) FROM transactions WHERE category = 'Interest') AS Interest,
|
(SELECT SUM(money_in) FROM transactions WHERE category = 'Interest') AS Interest,
|
||||||
(SELECT SUM(fee) * -1 FROM transactions) AS fees
|
(SELECT SUM(fee) * -1 FROM transactions) AS fees
|
||||||
FROM transactions LIMIT 1
|
FROM transactions LIMIT 1
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
List<Object[]> getTotals();
|
List<Object[]> getTotals();
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT (SUM(money_in) + SUM(money_out)) AS "Net Cashflow"
|
SELECT (SUM(money_in) + SUM(money_out)) AS "Net Cashflow"
|
||||||
FROM transactions
|
FROM transactions
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double getNetCashflow();
|
double getNetCashflow();
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT (SUM(money_in) + SUM(money_out)) / NULLIF(SUM(money_in), 0) AS "Savings Rate"
|
SELECT (SUM(money_in) + SUM(money_out)) / NULLIF(SUM(money_in), 0) AS "Savings Rate"
|
||||||
FROM transactions;
|
FROM transactions;
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
double getSavingRate();
|
double getSavingRate();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
package com.example.FinanceTracker.Services;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
|
import com.example.FinanceTracker.DTOs.Investments.CurrRate;
|
||||||
|
import com.example.FinanceTracker.DTOs.Investments.Currency;
|
||||||
|
import com.example.FinanceTracker.DTOs.Investments.ExchangeRate;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class InvestmentService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RestClient restClient;
|
||||||
|
|
||||||
|
private final int WEEK = 1;
|
||||||
|
private final int MONTH = 2;
|
||||||
|
private final int YEAR = 3;
|
||||||
|
|
||||||
|
public List<CurrRate> getCurrRates() {
|
||||||
|
return List.of("EUR", "USD", "JPY", "GBP", "CAD", "AUD").stream()
|
||||||
|
.map(code -> {
|
||||||
|
Double rate = restClient.get()
|
||||||
|
.uri(uriBuilder -> uriBuilder
|
||||||
|
.path("/exchangeRate/pair")
|
||||||
|
.queryParam("from", code)
|
||||||
|
.queryParam("to", "ZAR")
|
||||||
|
.build())
|
||||||
|
.retrieve()
|
||||||
|
.body(Double.class);
|
||||||
|
|
||||||
|
return new CurrRate(code, rate.toString(), getChange(code, "ZAR"));
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getChange(String targetCurr, String desiredCurrency) {
|
||||||
|
|
||||||
|
final String effectiveTargetCurr = targetCurr.toUpperCase().contains("USD") ? "ZAR" : targetCurr;
|
||||||
|
|
||||||
|
List<ExchangeRate> exchangeRate = restClient.get()
|
||||||
|
.uri(uriBuilder -> uriBuilder
|
||||||
|
.path("exchangeRate/historical")
|
||||||
|
.queryParam("targetCurrency", effectiveTargetCurr)
|
||||||
|
.queryParam("desiredCurrency", desiredCurrency)
|
||||||
|
.queryParam("fromDate", LocalDate.now().minusDays(1))
|
||||||
|
.queryParam("toDate", LocalDate.now().plusDays(1))
|
||||||
|
.build())
|
||||||
|
.retrieve()
|
||||||
|
.body(new ParameterizedTypeReference<List<ExchangeRate>>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exchangeRate.get(0).getRate().compareTo(exchangeRate.get(1).getRate()) > 0) {
|
||||||
|
return "UP";
|
||||||
|
} else {
|
||||||
|
return "DOWN";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ExchangeRate> getHistoricalRates(String targetCurr, int duration) {
|
||||||
|
|
||||||
|
LocalDate toDate = LocalDate.now().plusDays(1);
|
||||||
|
LocalDate fromDate;
|
||||||
|
|
||||||
|
switch (duration) {
|
||||||
|
case WEEK:
|
||||||
|
fromDate = LocalDate.now().minusWeeks(1);
|
||||||
|
break;
|
||||||
|
case MONTH:
|
||||||
|
fromDate = LocalDate.now().minusMonths(1);
|
||||||
|
break;
|
||||||
|
case YEAR:
|
||||||
|
fromDate = LocalDate.now().minusYears(1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fromDate = LocalDate.now().minusWeeks(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ExchangeRate> historicalRates = restClient.get()
|
||||||
|
.uri(uriBuilder -> uriBuilder
|
||||||
|
.path("exchangeRate/historical")
|
||||||
|
.queryParam("targetCurrency", targetCurr)
|
||||||
|
.queryParam("desiredCurrency", "ZAR")
|
||||||
|
.queryParam("fromDate", fromDate.toString())
|
||||||
|
.queryParam("toDate", toDate.toString())
|
||||||
|
.build())
|
||||||
|
.retrieve()
|
||||||
|
.body(new ParameterizedTypeReference<List<ExchangeRate>>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return historicalRates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Currency> getAllCurrencies() {
|
||||||
|
|
||||||
|
return restClient.get()
|
||||||
|
.uri("/currency/currencies")
|
||||||
|
.retrieve()
|
||||||
|
.body(new ParameterizedTypeReference<List<Currency>>() {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,39 +1,32 @@
|
||||||
<div th:fragment="overviewCard(card)" class="col-md-6 col-xl-3 mb-4">
|
<div th:fragment="overviewCard(card)" class="col-md-6 col-xl-3 mb-4">
|
||||||
<div class="card shadow-sm h-100 border-start border-4"
|
<div class="card shadow-sm h-100 text-center"
|
||||||
th:classappend="${card.borderColour}"
|
|
||||||
role="group"
|
role="group"
|
||||||
th:aria-label="${card.heading}">
|
th:aria-label="${card.heading}">
|
||||||
|
|
||||||
<div class="card-body d-flex align-items-center justify-content-between">
|
<div class="card-body">
|
||||||
<!-- Text Content -->
|
|
||||||
<div class="me-3">
|
|
||||||
<div class="text-uppercase fw-semibold small"
|
|
||||||
th:classappend="${card.textColour}">
|
|
||||||
<span th:text="${card.heading}">
|
|
||||||
Average Earnings (Monthly)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="fw-bold fs-4 text-dark">
|
<i class="material-icons fs-3 mb-2 text-muted"
|
||||||
<span th:text="${card.value}">$40,000</span>
|
th:text="${card.icon}"
|
||||||
</div>
|
aria-hidden="true">
|
||||||
|
attach_money
|
||||||
|
</i>
|
||||||
|
|
||||||
<!-- Optional subtext -->
|
<div class="fw-bold fs-5"
|
||||||
<div class="text-muted small"
|
th:text="${card.value}">
|
||||||
th:if="${card.subText}"
|
$40,000
|
||||||
th:text="${card.subText}">
|
|
||||||
Compared to last month
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Icon -->
|
<div class="text-uppercase fs-7 fw-semibold mt-1"
|
||||||
<div class="flex-shrink-0">
|
th:classappend="${card.textColour}">
|
||||||
<i class="material-icons fs-2 text-muted"
|
<span th:text="${card.heading}">Average Earnings</span>
|
||||||
th:text="${card.icon}"
|
|
||||||
aria-hidden="true">
|
|
||||||
attach_money
|
|
||||||
</i>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="small text-muted mt-1"
|
||||||
|
th:if="${card.subText}"
|
||||||
|
th:text="${card.subText}">
|
||||||
|
Compared to last month
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,16 @@
|
||||||
${currRate.change} == 'UP'
|
${currRate.change} == 'UP'
|
||||||
? ' border border-success text-success'
|
? ' border border-success text-success'
|
||||||
: ' border border-danger text-danger'
|
: ' border border-danger text-danger'
|
||||||
"
|
">
|
||||||
>
|
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="material-icons bg-gray"
|
class="material-icons bg-gray"
|
||||||
th:text="${currRate.change} == 'UP' ? 'trending_up' : 'trending_down'">
|
th:text="${currRate.change} == 'UP' ? 'trending_up' : 'trending_down'">
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="fw-semibold text-dark" th:text="${currRate.ISO}"></span>
|
<span class="fw-semibold text-dark" th:text="${currRate.ISO}"></span>
|
||||||
|
|
||||||
<span th:text="${currRate.value}"></span>
|
<span th:text="${currRate.value}"></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,9 +135,12 @@
|
||||||
<span class="fw-bold text-primary mb-1">Historical Rates</span>
|
<span class="fw-bold text-primary mb-1">Historical Rates</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4 mw-50">
|
<div class="col-lg-4 mw-50">
|
||||||
<div class="btn-group d-flex justify-content-center mb-1" role="group" aria-label="Time range">
|
<div class="btn-group d-flex justify-content-center mb-1" role="group"
|
||||||
<input type="radio" class="btn-check" name="options" id="option1"
|
aria-label="Time range">
|
||||||
autocomplete="off" checked>
|
<input th:hx-get="@{/historicalRatesWeekly}" hx-trigger="click once"
|
||||||
|
hx-swap="outerHTML" hx-target="#HistoricalRatesChartContainer"
|
||||||
|
type="radio" class="btn-check" name="historicalRatesWeekly"
|
||||||
|
id="option1" autocomplete="off" checked>
|
||||||
<label class="btn btn-secondary" for="option1">Week</label>
|
<label class="btn btn-secondary" for="option1">Week</label>
|
||||||
|
|
||||||
<input type="radio" class="btn-check" name="options" id="option2"
|
<input type="radio" class="btn-check" name="options" id="option2"
|
||||||
|
|
@ -151,7 +154,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-4 d-flex justify-content-end">
|
<div class="col-lg-4 d-flex justify-content-end">
|
||||||
<select class="form-select" name="" id="" style="max-width: 200px;">
|
<select class="form-select" name="currencyChange" id="" style="max-width: 200px;"
|
||||||
|
th:hx-get="@{/changeHistoricalCurr}"
|
||||||
|
hx-trigger="change"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target="#HistoricalRatesChartContainer">
|
||||||
<option th:each="currency : ${allCurrencies}"
|
<option th:each="currency : ${allCurrencies}"
|
||||||
th:value="${currency.code}"
|
th:value="${currency.code}"
|
||||||
th:text="${currency.code + ' - ' + currency.currencyName}">Test
|
th:text="${currency.code + ' - ' + currency.currencyName}">Test
|
||||||
|
|
@ -161,7 +168,44 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<canvas id="HistoricalRatesChart"></canvas>
|
<div id="HistoricalRatesChartContainer" th:fragment="HistoricalRatesChart"
|
||||||
|
style="height: 350px;">
|
||||||
|
|
||||||
|
|
||||||
|
<canvas id="HistoricalRatesChart"></canvas>
|
||||||
|
|
||||||
|
<script th:inline="javascript">
|
||||||
|
/*<![CDATA[*/
|
||||||
|
new Chart(document.getElementById('HistoricalRatesChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: /*[[${labels}]]*/[],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Rates',
|
||||||
|
data: /*[[${historicalRates}]]*/[],
|
||||||
|
borderColor: '#2E7D32',
|
||||||
|
backgroundColor: 'rgba(46,125,50,0.15)',
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true,
|
||||||
|
tension: 0
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: v => `$${v.toLocaleString()}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/*]]>*/
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -185,6 +229,40 @@
|
||||||
<script src="assets/js/chart.min.js"></script>
|
<script src="assets/js/chart.min.js"></script>
|
||||||
<script src="assets/js/bs-init.js"></script>
|
<script src="assets/js/bs-init.js"></script>
|
||||||
<script src="assets/js/theme.js"></script>
|
<script src="assets/js/theme.js"></script>
|
||||||
|
<script th:inline="javascript">
|
||||||
|
/*<![CDATA[*/
|
||||||
|
new Chart(document.getElementById('HistoricalRatesChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: /*[[${labels}]]*/[],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Rates',
|
||||||
|
data: /*[[${historicalRates}]]*/[],
|
||||||
|
borderColor: '#2E7D32',
|
||||||
|
backgroundColor: 'rgba(46,125,50,0.15)',
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true,
|
||||||
|
tension: 0
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: v => `$${v.toLocaleString()}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/*]]>*/
|
||||||
|
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -128,22 +128,22 @@
|
||||||
<table th:fragment="transactionTable" class="table my-0" id="dataTable">
|
<table th:fragment="transactionTable" class="table my-0" id="dataTable">
|
||||||
<!-- Fixed column widths -->
|
<!-- Fixed column widths -->
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style="width: 120px;"> <!-- Date -->
|
|
||||||
<col style="width: 130px;"> <!-- Money In -->
|
<col style="width: 130px;"> <!-- Money In -->
|
||||||
<col style="width: 130px;"> <!-- Money Out -->
|
<col style="width: 130px;"> <!-- Money Out -->
|
||||||
<col style="width: 140px;"> <!-- Balance -->
|
|
||||||
<col style="width: 110px;"> <!-- Fees -->
|
<col style="width: 110px;"> <!-- Fees -->
|
||||||
|
<col style="width: 140px;"> <!-- Balance -->
|
||||||
|
<col style="width: 120px;"> <!-- Date -->
|
||||||
<col style="width: 160px;"> <!-- Category -->
|
<col style="width: 160px;"> <!-- Category -->
|
||||||
<col style="width: 260px;"> <!-- Description -->
|
<col style="width: 260px;"> <!-- Description -->
|
||||||
</colgroup>
|
</colgroup>
|
||||||
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
|
||||||
<th class="numeric">Money In</th>
|
<th class="numeric">Money In</th>
|
||||||
<th class="numeric">Money Out</th>
|
<th class="numeric">Money Out</th>
|
||||||
<th class="numeric">Balance</th>
|
|
||||||
<th class="numeric">Fees</th>
|
<th class="numeric">Fees</th>
|
||||||
|
<th class="numeric">Balance</th>
|
||||||
|
<th>Date</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
|
|
||||||
|
|
@ -154,15 +154,15 @@
|
||||||
<tr th:each="transaction : ${transactions}"
|
<tr th:each="transaction : ${transactions}"
|
||||||
th:with="rowClass=${(transaction.moneyIn + transaction.moneyOut + transaction.fee) < 0 ? 'txn-negative' : 'txn-positive'}"
|
th:with="rowClass=${(transaction.moneyIn + transaction.moneyOut + transaction.fee) < 0 ? 'txn-negative' : 'txn-positive'}"
|
||||||
th:class="${rowClass}">
|
th:class="${rowClass}">
|
||||||
<td th:text="${transaction.date}"></td>
|
|
||||||
<td class="numeric"
|
<td class="numeric"
|
||||||
th:text="${#numbers.formatCurrency(transaction.moneyIn)}"></td>
|
th:text="${#numbers.formatCurrency(transaction.moneyIn)}"></td>
|
||||||
<td class="numeric"
|
<td class="numeric"
|
||||||
th:text="${#numbers.formatCurrency(transaction.moneyOut)}"></td>
|
th:text="${#numbers.formatCurrency(transaction.moneyOut)}"></td>
|
||||||
<td class="numeric"
|
|
||||||
th:text="${#numbers.formatCurrency(transaction.balance)}"></td>
|
|
||||||
<td class="numeric" th:text="${#numbers.formatCurrency(transaction.fee)}">
|
<td class="numeric" th:text="${#numbers.formatCurrency(transaction.fee)}">
|
||||||
</td>
|
</td>
|
||||||
|
<td class="numeric"
|
||||||
|
th:text="${#numbers.formatCurrency(transaction.balance)}"></td>
|
||||||
|
<td th:text="${transaction.date}"></td>
|
||||||
<td th:text="${transaction.category}"></td>
|
<td th:text="${transaction.category}"></td>
|
||||||
<td class="description" th:text="${transaction.description}"></td>
|
<td class="description" th:text="${transaction.description}"></td>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue