- Refactored transcations table and queries to use catetories table

- Added historical rates chart for investments page
This commit is contained in:
Kiyan 2026-02-12 21:49:33 +02:00
parent 60ddc816a0
commit f5b3e49ca6
12 changed files with 449 additions and 235 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 162 KiB

View File

@ -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);

View File

@ -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",

View File

@ -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";
} }
} }

View File

@ -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;

View File

@ -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);
} }

View File

@ -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();
} }

View File

@ -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>>() {
});
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">Fees</th>
<th class="numeric">Balance</th> <th class="numeric">Balance</th>
<th class="numeric">Fees</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>