Refactor Home, Investments, and Transactions Controllers; enhance transaction pagination; update investment historical rates chart;
This commit is contained in:
parent
052b3a5fed
commit
975b36d097
|
|
@ -34,7 +34,7 @@ public class PdfManager {
|
|||
public List<Transaction> extractTransactions() {
|
||||
|
||||
PagePdfDocumentReader pdfReader =
|
||||
new PagePdfDocumentReader("/static/statements/account_statement_1-Jan-2024_to_2-Feb-2026.pdf");
|
||||
new PagePdfDocumentReader("/static/statements/statement.pdf");
|
||||
|
||||
List<Document> documents = pdfReader.get();
|
||||
StringBuilder documentText = new StringBuilder();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
package com.example.FinanceTracker.Controllers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
|
|
@ -11,6 +16,11 @@ import org.springframework.ui.Model;
|
|||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import com.example.FinanceTracker.Components.PdfManager;
|
||||
import com.example.FinanceTracker.Models.Transaction;
|
||||
import com.example.FinanceTracker.Repo.CategoriesRepo;
|
||||
import com.example.FinanceTracker.Services.TransactionService;
|
||||
|
|
@ -24,44 +34,68 @@ public class AddTransaction {
|
|||
@Autowired
|
||||
private CategoriesRepo categoriesRepo;
|
||||
|
||||
@Autowired
|
||||
private PdfManager pdfManager;
|
||||
|
||||
// Define the upload directory
|
||||
private static final String UPLOAD_DIR = "src/main/resources/static/statements/";
|
||||
|
||||
@GetMapping("/addTransaction")
|
||||
public String addTransaction(Model model) {
|
||||
|
||||
model.addAttribute("transaction", new Transaction());
|
||||
|
||||
List<String> categoriesNames = categoriesRepo.findAllCategoryNames();
|
||||
model.addAttribute("categories", categoriesNames);
|
||||
|
||||
// footer
|
||||
model.addAttribute("footerDetails", "Copyright © FinanceTracker " + LocalDate.now().getYear());
|
||||
|
||||
return "add-transaction";
|
||||
}
|
||||
|
||||
@PostMapping("/addTranaction")
|
||||
public String addNewTranactions(@ModelAttribute Transaction transaction, Model model) {
|
||||
|
||||
transactionValidation(transaction);
|
||||
|
||||
transactionService.save(transaction);
|
||||
|
||||
// Set Category Dropdown menu
|
||||
List<String> categoriesNames = categoriesRepo.findAllCategoryNames();
|
||||
model.addAttribute("categories", categoriesNames);
|
||||
|
||||
return "add-transaction";
|
||||
}
|
||||
|
||||
@PostMapping("/uploadStatement")
|
||||
public String uploadStatement(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {
|
||||
if (file.isEmpty() || !file.getOriginalFilename().endsWith(".pdf")) {
|
||||
redirectAttributes.addFlashAttribute("message", "Please select a valid PDF file.");
|
||||
return "redirect:/addTransaction";
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure directory exists
|
||||
Path uploadPath = Paths.get(UPLOAD_DIR);
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
}
|
||||
|
||||
// Resolve file path and save (Replace existing)
|
||||
Path filePath = uploadPath.resolve("statement.pdf");
|
||||
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
redirectAttributes.addFlashAttribute("message",
|
||||
"File uploaded successfully: " + file.getOriginalFilename());
|
||||
|
||||
|
||||
transactionService.saveAll(pdfManager.extractTransactions());
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
redirectAttributes.addFlashAttribute("message", "Failed to upload file.");
|
||||
}
|
||||
|
||||
return "redirect:/addTransaction";
|
||||
}
|
||||
|
||||
private void transactionValidation(Transaction transaction) {
|
||||
|
||||
// Set Balance
|
||||
BigDecimal balance = new BigDecimal(transactionService.getLatestBalance());
|
||||
transaction.setBalance(
|
||||
balance.add(transaction.getMoneyIn())
|
||||
.add(transaction.getFee()).setScale(2, RoundingMode.HALF_UP));
|
||||
|
||||
// Set Money In/Money Out
|
||||
if (transaction.getMoneyIn().compareTo(BigDecimal.ZERO) > 0) {
|
||||
transaction.setMoneyOut(new BigDecimal(0));
|
||||
} else if ((transaction.getMoneyIn().compareTo(BigDecimal.ZERO) < 0)) {
|
||||
|
|
@ -69,11 +103,8 @@ public class AddTransaction {
|
|||
transaction.setMoneyIn(new BigDecimal(0));
|
||||
}
|
||||
|
||||
// Handle Fees
|
||||
if (transaction.getFee() == null) {
|
||||
transaction.setFee(new BigDecimal(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ import org.springframework.ui.Model;
|
|||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import com.example.FinanceTracker.Components.BootstrapColours;
|
||||
//import com.example.FinanceTracker.Components.PdfManager;
|
||||
import com.example.FinanceTracker.DTOs.GroupedTransaction;
|
||||
import com.example.FinanceTracker.DTOs.Home.OverviewCard;
|
||||
import com.example.FinanceTracker.Models.Transaction;
|
||||
|
|
@ -25,10 +24,6 @@ import com.example.FinanceTracker.Services.TransactionService;
|
|||
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
// @Autowired
|
||||
// private PdfManager pdfManager;
|
||||
|
||||
@Autowired
|
||||
private TransactionService transactionsService;
|
||||
|
||||
|
|
@ -41,8 +36,6 @@ public class HomeController {
|
|||
@GetMapping("/")
|
||||
public String Home(Model model) {
|
||||
|
||||
//transactionsService.saveAll(pdfManager.extractTransactions());
|
||||
|
||||
// Balance Text
|
||||
model.addAttribute("latestBalance",
|
||||
"Balance: " + currencyFormat.format(transactionsService.getLatestBalance()) + "*");
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.example.FinanceTracker.Controllers;
|
|||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
|
@ -10,6 +11,7 @@ import org.springframework.stereotype.Controller;
|
|||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import com.example.FinanceTracker.DTOs.Investments.CurrRate;
|
||||
import com.example.FinanceTracker.DTOs.Investments.Currency;
|
||||
import com.example.FinanceTracker.DTOs.Investments.ExchangeRate;
|
||||
|
|
@ -27,66 +29,53 @@ public class InvestmentsController {
|
|||
|
||||
@GetMapping("/investments")
|
||||
public String investments(Model model) {
|
||||
|
||||
// Heading
|
||||
// Stats and Header
|
||||
model.addAttribute("latestBalance", "Total Invested: " + transactionService.getTotalInvested() + "*");
|
||||
|
||||
// Create Currency Pills
|
||||
List<CurrRate> allRates = investmentService.getCurrRates();
|
||||
model.addAttribute("currRates", allRates);
|
||||
|
||||
// Populate Historical Rates Dropdown
|
||||
List<Currency> allCurrencies = investmentService.getAllCurrencies();
|
||||
model.addAttribute("allCurrencies", allCurrencies);
|
||||
|
||||
// historical rates chart
|
||||
List<ExchangeRate> historicalExchangeRates = investmentService.getHistoricalRates("USD", 1).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);
|
||||
|
||||
// footer
|
||||
// Navigation / UI Elements
|
||||
model.addAttribute("currRates", investmentService.getCurrRates());
|
||||
model.addAttribute("allCurrencies", investmentService.getAllCurrencies());
|
||||
model.addAttribute("footerDetails", "Copyright © FinanceTracker " + LocalDate.now().getYear());
|
||||
|
||||
// Default Chart Data: USD for 1 Week
|
||||
populateChartModel("USD", "WEEK", model);
|
||||
|
||||
return "investments";
|
||||
}
|
||||
|
||||
@GetMapping("/historicalRates")
|
||||
public String getHistoricalRates(
|
||||
@RequestParam("duration") String durationCode,
|
||||
@RequestParam("currencyChange") String currencyChange,
|
||||
@RequestParam(value = "duration", defaultValue = "WEEK") String durationCode,
|
||||
@RequestParam(value = "currencyChange", defaultValue = "USD") String currencyChange,
|
||||
Model model) {
|
||||
|
||||
// historical rates chart
|
||||
int duration = 1;
|
||||
if (durationCode.equals("MONTH"))
|
||||
duration = 2;
|
||||
if (durationCode.equals("YEAR"))
|
||||
duration = 3;
|
||||
|
||||
List<ExchangeRate> historicalExchangeRates = investmentService.getHistoricalRates(currencyChange, duration)
|
||||
.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);
|
||||
populateChartModel(currencyChange, durationCode, model);
|
||||
|
||||
// Return only the fragment for HTMX to inject
|
||||
return "investments :: HistoricalRatesChart";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to process business logic for chart data
|
||||
*/
|
||||
private void populateChartModel(String currencyCode, String durationCode, Model model) {
|
||||
int durationKey = 1;
|
||||
if ("MONTH".equals(durationCode)) durationKey = 2;
|
||||
if ("YEAR".equals(durationCode)) durationKey = 3;
|
||||
|
||||
// Fetch rates and ensure they are sorted by date ascending for the chart
|
||||
List<ExchangeRate> ratesList = investmentService.getHistoricalRates(currencyCode, durationKey);
|
||||
Collections.reverse(ratesList);
|
||||
|
||||
List<BigDecimal> data = new ArrayList<>();
|
||||
List<LocalDate> labels = new ArrayList<>();
|
||||
|
||||
for (ExchangeRate er : ratesList) {
|
||||
data.add(er.getRate());
|
||||
labels.add(er.getCreatedAt().toLocalDate());
|
||||
}
|
||||
|
||||
model.addAttribute("historicalRates", data);
|
||||
model.addAttribute("labels", labels);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,102 +1,58 @@
|
|||
package com.example.FinanceTracker.Controllers;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import com.example.FinanceTracker.Models.Transaction;
|
||||
import com.example.FinanceTracker.Services.TransactionService;
|
||||
|
||||
@Controller
|
||||
public class TransactionsController {
|
||||
private int pageNum = 0;
|
||||
private int pageSize = 10;
|
||||
|
||||
@Autowired
|
||||
private TransactionService transactionService;
|
||||
|
||||
// Main Page Load
|
||||
@GetMapping("/transactions")
|
||||
public String transactions(Model model) {
|
||||
public String transactions(
|
||||
Model model,
|
||||
@RequestParam(defaultValue = "0") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize) {
|
||||
|
||||
Page<Transaction> pageResults = transactionService.findAllPaged(pageNum, pageSize);
|
||||
|
||||
model.addAttribute("pageNum", pageNum);
|
||||
model.addAttribute("pageSize", pageResults.getTotalPages());
|
||||
model.addAttribute("transactionCount", pageResults.getNumberOfElements());
|
||||
model.addAttribute("transactions", pageResults);
|
||||
|
||||
// footer
|
||||
prepareModel(model, "", pageNum, pageSize);
|
||||
model.addAttribute("footerDetails", "Copyright © FinanceTracker " + LocalDate.now().getYear());
|
||||
|
||||
return "transactions";
|
||||
}
|
||||
|
||||
@GetMapping("/changePageSize")
|
||||
public Collection<ModelAndView> changePageSize(
|
||||
@RequestParam("pageSizeCB") int pageSizeCB) {
|
||||
this.pageSize = pageSizeCB;
|
||||
this.pageNum = 0;
|
||||
@GetMapping({ "/transactions-fragment", "/searchTransactions" })
|
||||
public String getTransactionsFragment(
|
||||
Model model,
|
||||
@RequestParam(value = "searchValue", required = false, defaultValue = "") String searchValue,
|
||||
@RequestParam(defaultValue = "0") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize) {
|
||||
|
||||
Page<Transaction> results = transactionService.findAllPaged(pageNum, pageSize);
|
||||
return List.of(
|
||||
new ModelAndView("transactions :: transactionTable",
|
||||
Map.of("transactions", results)),
|
||||
setPageDetails(results));
|
||||
prepareModel(model, searchValue, pageNum, pageSize);
|
||||
// Returns only the specific fragment inside transactions.html
|
||||
return "transactions :: transactionList";
|
||||
}
|
||||
|
||||
@GetMapping("/searchTransactions")
|
||||
public Collection<ModelAndView> SearchTable(@RequestParam("searchValue") String searchValue) {
|
||||
this.pageNum = 0;
|
||||
Page<Transaction> searchResults = transactionService.searchTransactions(searchValue, pageNum, pageSize);
|
||||
|
||||
return List.of(
|
||||
new ModelAndView("transactions :: transactionTable",
|
||||
Map.of("transactions", searchResults)),
|
||||
setPageDetails(searchResults));
|
||||
private void prepareModel(Model model, String search, int page, int size) {
|
||||
Page<Transaction> results;
|
||||
if (search == null || search.isBlank()) {
|
||||
results = transactionService.findAllPaged(page, size);
|
||||
} else {
|
||||
results = transactionService.searchTransactions(search, page, size);
|
||||
}
|
||||
|
||||
@GetMapping("/previousPage")
|
||||
public Collection<ModelAndView> previousPage() {
|
||||
|
||||
if (pageNum > 0) {
|
||||
pageNum--;
|
||||
}
|
||||
Page<Transaction> results = transactionService.findAllPaged(pageNum, pageSize);
|
||||
return List.of(
|
||||
new ModelAndView("transactions :: transactionTable",
|
||||
Map.of("transactions", results)),
|
||||
setPageDetails(results));
|
||||
}
|
||||
|
||||
@GetMapping("/nextPage")
|
||||
public Collection<ModelAndView> nextPage() {
|
||||
|
||||
Page<Transaction> current = transactionService.findAllPaged(pageNum, pageSize);
|
||||
|
||||
if (pageNum < current.getTotalPages() - 1) {
|
||||
pageNum++;
|
||||
}
|
||||
Page<Transaction> results = transactionService.findAllPaged(pageNum, pageSize);
|
||||
return List.of(
|
||||
new ModelAndView("transactions :: transactionTable",
|
||||
Map.of("transactions", results)),
|
||||
setPageDetails(results));
|
||||
}
|
||||
|
||||
private ModelAndView setPageDetails(Page<?> page) {
|
||||
return new ModelAndView("transactions :: tableInfo",
|
||||
Map.of("pageNum", pageNum, "pageSize", page.getTotalPages(), "transactionCount",
|
||||
page.getNumberOfElements()));
|
||||
|
||||
model.addAttribute("transactions", results.getContent());
|
||||
model.addAttribute("pageNum", page);
|
||||
model.addAttribute("totalPages", results.getTotalPages());
|
||||
model.addAttribute("searchValue", search);
|
||||
model.addAttribute("pageSize", size);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ import com.example.FinanceTracker.Models.Transaction;
|
|||
public interface TransactionRepo extends JpaRepository<Transaction, UUID> {
|
||||
|
||||
@Query(value = """
|
||||
SELECT *
|
||||
FROM transactions
|
||||
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
|
||||
WHERE date BETWEEN :fromDate AND :toDate
|
||||
ORDER BY date
|
||||
""", nativeQuery = true)
|
||||
|
|
@ -85,6 +86,7 @@ public interface TransactionRepo extends JpaRepository<Transaction, UUID> {
|
|||
FROM transactions t JOIN categories c ON t.category = c.id
|
||||
WHERE money_in != 0
|
||||
GROUP BY category
|
||||
ORDER BY SUM(money_in) DESC
|
||||
""", nativeQuery = true)
|
||||
List<GroupedTransaction> getIncomeGroupedTransactions();
|
||||
|
||||
|
|
@ -110,6 +112,8 @@ public interface TransactionRepo extends JpaRepository<Transaction, UUID> {
|
|||
FROM transactions t JOIN categories c ON t.category = c.id
|
||||
WHERE money_out != 0
|
||||
GROUP BY category
|
||||
ORDER BY SUM(money_out)
|
||||
LIMIT 10
|
||||
""", nativeQuery = true)
|
||||
List<GroupedTransaction> getExpenditureGroupedTransactions();
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -8,68 +8,19 @@
|
|||
<title>FinanceTracker | Add Transaction</title>
|
||||
|
||||
<link rel="stylesheet" th:href="@{/assets/bootstrap/css/bootstrap.min.css}">
|
||||
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fc;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
vertical-align: middle;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: #f8f9fc;
|
||||
border-right: none;
|
||||
color: #4e73df;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
box-shadow: none;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
.input-group:focus-within .input-group-text {
|
||||
border-color: #86b7fe;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.scroll-to-top {
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #0d6efd;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
body { background-color: #f8f9fc; }
|
||||
.card { border: none; border-radius: 0.75rem; }
|
||||
.card-header { background: transparent; border-bottom: 1px solid rgba(0, 0, 0, 0.05); }
|
||||
.material-icons { vertical-align: middle; font-size: 20px; }
|
||||
.input-group-text { background-color: #f8f9fc; border-right: none; color: #4e73df; }
|
||||
.form-control, .form-select { border-left: none; }
|
||||
.form-control:focus, .form-select:focus { box-shadow: none; border-color: #dee2e6; }
|
||||
.input-group:focus-within .input-group-text { border-color: #86b7fe; }
|
||||
label { font-weight: 600; color: #495057; margin-bottom: 0.4rem; font-size: 0.85rem; }
|
||||
.scroll-to-top { position: fixed; right: 1rem; bottom: 1rem; width: 40px; height: 40px; background-color: #0d6efd; color: #fff; display: flex; align-items: center; justify-content: center; text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
@ -83,9 +34,14 @@
|
|||
|
||||
<div class="container-fluid px-4">
|
||||
|
||||
<div th:if="${message}" class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
<span th:text="${message}"></span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="mb-0">Add Transaction</h3>
|
||||
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#exampleModal">
|
||||
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
|
||||
<span class="material-icons me-1">upload_file</span>
|
||||
Bulk Upload
|
||||
</button>
|
||||
|
|
@ -161,23 +117,25 @@
|
|||
|
||||
<div th:replace="~{fragments/footer :: footer}"></div>
|
||||
|
||||
<div class="modal fade" id="exampleModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal fade" id="uploadModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<form th:action="@{/uploadStatement}" method="post" enctype="multipart/form-data">
|
||||
<div class="modal-header border-bottom-0">
|
||||
<h5 class="modal-title fw-bold">Import CSV</h5>
|
||||
<h5 class="modal-title fw-bold">Import Statement</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body py-4">
|
||||
<p class="text-muted small">Select a .csv or .xlsx file to import multiple transactions at once.</p>
|
||||
<p class="text-muted small">Select a .pdf file to import. Existing files with the same name will be overwritten.</p>
|
||||
<div class="input-group">
|
||||
<input type="file" class="form-control" id="inputGroupFile02" style="border-left: 1px solid #dee2e6;">
|
||||
<input type="file" name="file" class="form-control" id="inputGroupFile02" accept=".pdf" required style="border-left: 1px solid #dee2e6;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-top-0">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary">Process Upload</button>
|
||||
<button type="submit" class="btn btn-primary">Process Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -188,11 +146,9 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
||||
<script th:src="@{/assets/bootstrap/js/bootstrap.min.js}"></script>
|
||||
<script th:src="@{/assets/js/bs-init.js}"></script>
|
||||
<script th:src="@{/assets/js/theme.js}"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -113,8 +113,7 @@
|
|||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="mb-0">Dashboard</h3>
|
||||
<a th:hx-get="@{/summary}" hx-trigger="click" hx-swap="none"
|
||||
class="btn btn-sm btn-primary">
|
||||
<a th:hx-get="@{/summary}" hx-trigger="click" hx-swap="none" class="btn btn-sm btn-primary">
|
||||
<span class="material-icons me-1">description</span>
|
||||
Generate Report
|
||||
</a>
|
||||
|
|
@ -216,17 +215,17 @@
|
|||
|
||||
</div>
|
||||
|
||||
<!-- Insights Panel -->
|
||||
<!-- Logs Panel -->
|
||||
<div class="col-lg-3">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-header d-flex align-items-center gap-2">
|
||||
<span class="material-icons text-primary">insights</span>
|
||||
<h6 class="fw-bold text-primary mb-0">Insights</h6>
|
||||
<span class="material-icons text-primary">Logs</span>
|
||||
<h6 class="fw-bold text-primary mb-0">Logs</h6>
|
||||
</div>
|
||||
<div class="card-body small">
|
||||
|
||||
<div class="insight-item">
|
||||
<div class="insight-icon">
|
||||
<div class="log-item">
|
||||
<div class="log-icon">
|
||||
<span class="material-icons">warning</span>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -235,8 +234,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="insight-item">
|
||||
<div class="insight-icon">
|
||||
<div class="log-item">
|
||||
<div class="log-icon">
|
||||
<span class="material-icons">savings</span>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -245,8 +244,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="insight-item">
|
||||
<div class="insight-icon">
|
||||
<div class="log-item">
|
||||
<div class="log-icon">
|
||||
<span class="material-icons">calendar_month</span>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -308,7 +307,8 @@
|
|||
type: /*[[${incomeChartType}]]*/ 'doughnut',
|
||||
data: /*[[${incomeDoughnutdata}]]*/[],
|
||||
labels: /*[[${incomeDoughnutLabels}]]*/[],
|
||||
colours: /*[[${incomeDoughnutColours}]]*/[]
|
||||
//colours: /*[[${incomeDoughnutColours}]]*/[]
|
||||
colours: ['#126618', '#1A7A1F', '#238F25', '#2FA42C', '#3CB833', '#58C447', '#76CF65', '#94DB83', '#B2E6A1', '#D0F0C0']
|
||||
});
|
||||
|
||||
renderChart({
|
||||
|
|
@ -316,7 +316,8 @@
|
|||
type: /*[[${expenseChartType}]]*/ 'doughnut',
|
||||
data: /*[[${expenseDoughnutdata}]]*/[],
|
||||
labels: /*[[${expenseDoughnutLabels}]]*/[],
|
||||
colours: /*[[${expenseDoughnutColours}]]*/[]
|
||||
//colours: /*[[${expenseDoughnutColours}]]*/[]
|
||||
colours: ['#731A1A', '#8A2020', '#A12727', '#B82D2D', '#CF3333', '#D74D4D', '#DE6666', '#E68080', '#ED9A9A', '#F4B4B4']
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('categoryBarChart'), {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,7 @@
|
|||
<meta name="description" content="Personal finance dashboard showing income, expenses, and transactions">
|
||||
<title>FinanceTracker | Investments</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link rel="stylesheet" th:href="@{/assets/bootstrap/css/bootstrap.min.css}">
|
||||
|
||||
<!-- Material Icons -->
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
|
|
@ -18,111 +15,29 @@
|
|||
--positive-bg: rgba(40, 167, 69, 0.12);
|
||||
--negative-bg: rgba(220, 53, 69, 0.12);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f8f9fc;
|
||||
}
|
||||
|
||||
.txn-positive td {
|
||||
background-color: var(--positive-bg);
|
||||
}
|
||||
|
||||
.txn-negative td {
|
||||
background-color: var(--negative-bg);
|
||||
}
|
||||
|
||||
.numeric {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
position: relative;
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
vertical-align: middle;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.insight-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #f8f9fc;
|
||||
}
|
||||
|
||||
.insight-item+.insight-item {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.insight-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #e9ecef;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.scroll-to-top {
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #0d6efd;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
body { background-color: #f8f9fc; }
|
||||
.chart-area { position: relative; min-height: 260px; }
|
||||
.card { border: none; border-radius: 0.75rem; }
|
||||
.card-header { background: transparent; border-bottom: 1px solid rgba(0, 0, 0, 0.05); }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body id="page-top">
|
||||
<div id="wrapper">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div th:replace="~{fragments/sidebar-nav :: sidebar-nav}"></div>
|
||||
|
||||
<div id="content-wrapper" class="d-flex flex-column">
|
||||
<div id="content">
|
||||
|
||||
<!-- Topbar -->
|
||||
<div th:replace="~{fragments/topbar :: topbar}"></div>
|
||||
|
||||
<div class="container-fluid px-4">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="mb-0">Investments</h3>
|
||||
<!-- <a th:hx-get="@{/summary}" hx-trigger="click" hx-swap="none" class="btn btn-sm btn-primary">
|
||||
<span class="material-icons me-1">description</span>
|
||||
Generate Report
|
||||
</a> -->
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div th:each="rate : ${currRates}" class="col-lg-2 col-md-3 col-sm-4 mb-2 text-center">
|
||||
<span th:replace="~{fragments/investments/curr-badges :: curr-pill(currRate=${rate})}">
|
||||
</span>
|
||||
<span th:replace="~{fragments/investments/curr-badges :: curr-pill(currRate=${rate})}"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -130,59 +45,48 @@
|
|||
<div class="col-lg-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 ">
|
||||
<span class="fw-bold text-primary mb-1">Historical Rates</span>
|
||||
</div>
|
||||
<form id="historicalRatesControls" >
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold text-primary">Historical Rates</span>
|
||||
|
||||
<!-- Duration Radios -->
|
||||
<div class="btn-group d-flex justify-content-center mb-1" role="group">
|
||||
<input type="radio" name="duration" value="WEEK" class="btn-check"
|
||||
id="option1" checked hx-get="/historicalRates" hx-trigger="change"
|
||||
hx-target="#HistoricalRatesChartContainer" hx-swap="outerHTML"
|
||||
hx-include="#historicalRatesControls">
|
||||
<label class="btn btn-secondary" for="option1">Week</label>
|
||||
<form id="historicalRatesControls" class="d-flex gap-2">
|
||||
<div class="btn-group" role="group">
|
||||
<input type="radio" name="duration" value="WEEK" class="btn-check" id="opt1" checked
|
||||
hx-get="/historicalRates" hx-target="#HistoricalRatesChartContainer" hx-include="#historicalRatesControls">
|
||||
<label class="btn btn-outline-secondary btn-sm" for="opt1">Week</label>
|
||||
|
||||
<input type="radio" name="duration" value="MONTH" class="btn-check"
|
||||
id="option2" hx-get="/historicalRates" hx-trigger="change"
|
||||
hx-target="#HistoricalRatesChartContainer" hx-swap="outerHTML"
|
||||
hx-include="#historicalRatesControls">
|
||||
<label class="btn btn-secondary" for="option2">Month</label>
|
||||
<input type="radio" name="duration" value="MONTH" class="btn-check" id="opt2"
|
||||
hx-get="/historicalRates" hx-target="#HistoricalRatesChartContainer" hx-include="#historicalRatesControls">
|
||||
<label class="btn btn-outline-secondary btn-sm" for="opt2">Month</label>
|
||||
|
||||
<input type="radio" name="duration" value="YEAR" class="btn-check"
|
||||
id="option3" hx-get="/historicalRates" hx-trigger="change"
|
||||
hx-target="#HistoricalRatesChartContainer" hx-swap="outerHTML"
|
||||
hx-include="#historicalRatesControls">
|
||||
<label class="btn btn-secondary" for="option3">Year</label>
|
||||
<input type="radio" name="duration" value="YEAR" class="btn-check" id="opt3"
|
||||
hx-get="/historicalRates" hx-target="#HistoricalRatesChartContainer" hx-include="#historicalRatesControls">
|
||||
<label class="btn btn-outline-secondary btn-sm" for="opt3">Year</label>
|
||||
</div>
|
||||
|
||||
<!-- Currency Select -->
|
||||
<select class="form-select" name="currencyChange" style="max-width: 200px;"
|
||||
hx-get="/historicalRates" hx-trigger="change"
|
||||
hx-target="#HistoricalRatesChartContainer" hx-swap="outerHTML"
|
||||
hx-include="#historicalRatesControls">
|
||||
|
||||
<select class="form-select form-select-sm" name="currencyChange" style="width: auto;"
|
||||
hx-get="/historicalRates" hx-target="#HistoricalRatesChartContainer" hx-include="#historicalRatesControls">
|
||||
<option th:each="currency : ${allCurrencies}"
|
||||
th:selected="${currency.code == 'USD'}"
|
||||
th:value="${currency.code}"
|
||||
th:text="${currency.code + ' - ' + currency.currencyName}">
|
||||
</option>
|
||||
</select>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="HistoricalRatesChartContainer" th:fragment="HistoricalRatesChart"
|
||||
style="height: 350px;">
|
||||
|
||||
|
||||
<div id="HistoricalRatesChartContainer" th:fragment="HistoricalRatesChart" style="height: 350px; width: 100%;">
|
||||
<canvas id="HistoricalRatesChart"></canvas>
|
||||
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
new Chart(document.getElementById('HistoricalRatesChart'), {
|
||||
(function() {
|
||||
const ctx = document.getElementById('HistoricalRatesChart');
|
||||
// Destroy existing chart instance to prevent memory leaks and scaling issues
|
||||
const existingChart = Chart.getChart(ctx);
|
||||
if (existingChart) {
|
||||
existingChart.destroy();
|
||||
}
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: /*[[${labels}]]*/ [],
|
||||
|
|
@ -193,82 +97,38 @@
|
|||
backgroundColor: 'rgba(46,125,50,0.15)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 500 },
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
beginAtZero: false,
|
||||
ticks: {
|
||||
callback: v => `$${v.toLocaleString()}`
|
||||
callback: v => v.toLocaleString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
/*]]>*/
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div th:replace="~{fragments/footer :: footer}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="scroll-to-top rounded" href="#page-top">
|
||||
<span class="material-icons">keyboard_arrow_up</span>
|
||||
</a>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
||||
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
||||
<script src="assets/js/chart.min.js"></script>
|
||||
<script src="assets/js/bs-init.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>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -77,80 +77,57 @@
|
|||
|
||||
<body id="page-top">
|
||||
<div id="wrapper">
|
||||
<!--Sidebar - Navigation-->
|
||||
<div th:replace="~{fragments/sidebar-nav :: sidebar-nav}"></div>
|
||||
|
||||
|
||||
<div class="d-flex flex-column" id="content-wrapper">
|
||||
<div id="content">
|
||||
<!-- Topbar -->
|
||||
<div th:replace="~{fragments/topbar :: topbar}"></div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="card shadow">
|
||||
<div class="d-inline-flex align-items-center card-header py-3">
|
||||
<p class="text-primary m-0 fw-bold w-100">Transactions</p>
|
||||
<a th:href="@{/addTransaction}" class="d-inline-flex btn btn-primary">
|
||||
<i class="bi bi-plus"></i>Add
|
||||
</a>
|
||||
<div class="row align-items-center card-header py-3">
|
||||
<div class="col-md-4">
|
||||
<p class="text-primary m-0 fw-bold">Transactions</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-center">
|
||||
<input hx-get="/searchTransactions" hx-trigger="input changed delay:500ms"
|
||||
hx-target="#table-container" hx-include="#itemSize" name="searchValue"
|
||||
id="searchValue" type="search" class="form-control form-control-sm"
|
||||
placeholder="Search...">
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
<a th:href="@{/addTransaction}" class="btn btn-primary btn-sm">Add Transaction</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-nowrap">
|
||||
<div id="dataTable_length" class="dataTables_length" aria-controls="dataTable">
|
||||
<label class="form-label">Show
|
||||
<select th:hx-get="@{/changePageSize}" hx-target="#dataTable"
|
||||
hx-trigger="input changed" hx-swap="outerHtml" name="pageSizeCB"
|
||||
class="d-inline-block form-select form-select-sm">
|
||||
<option value="10" selected="">10</option>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Show
|
||||
<select id="itemSize" name="pageSize" hx-get="/transactions-fragment"
|
||||
hx-trigger="change" hx-target="#table-container" hx-include="#searchValue"
|
||||
class="d-inline-block form-select form-select-sm" style="width: auto;">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="text-md-end dataTables_filter" id="dataTable_filter">
|
||||
<label class="form-label">
|
||||
<input hx-trigger="input changed delay:1s" hx-swap="outerHtml"
|
||||
hx-target="#dataTable" hx-get="/searchTransactions" type="search"
|
||||
class="form-control form-control-sm" aria-controls="dataTable"
|
||||
name="searchValue" placeholder="Search">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="table-container" th:fragment="transactionList">
|
||||
<div class="table-responsive" style="overflow-x: auto;">
|
||||
<table th:fragment="transactionTable" class="table my-0" id="dataTable">
|
||||
<!-- Fixed column widths -->
|
||||
<colgroup>
|
||||
<col style="width: 130px;"> <!-- Money In -->
|
||||
<col style="width: 130px;"> <!-- Money Out -->
|
||||
<col style="width: 110px;"> <!-- Fees -->
|
||||
<col style="width: 140px;"> <!-- Balance -->
|
||||
<col style="width: 120px;"> <!-- Date -->
|
||||
<col style="width: 160px;"> <!-- Category -->
|
||||
<col style="width: 260px;"> <!-- Description -->
|
||||
</colgroup>
|
||||
|
||||
<table class="table my-0" id="dataTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="numeric">Money In</th>
|
||||
<th class="numeric">Money Out</th>
|
||||
<th class="numeric">Fees</th>
|
||||
<th class="numeric">Balance</th>
|
||||
<th>Date</th>
|
||||
<th>Category</th>
|
||||
<th>Description</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr th:each="transaction : ${transactions}"
|
||||
th:with="rowClass=${(transaction.moneyIn + transaction.moneyOut + transaction.fee) < 0 ? 'txn-negative' : 'txn-positive'}"
|
||||
th:class="${rowClass}">
|
||||
|
|
@ -158,44 +135,36 @@
|
|||
th:text="${#numbers.formatCurrency(transaction.moneyIn)}"></td>
|
||||
<td class="numeric"
|
||||
th:text="${#numbers.formatCurrency(transaction.moneyOut)}"></td>
|
||||
<td class="numeric" th:text="${#numbers.formatCurrency(transaction.fee)}">
|
||||
<td class="numeric"
|
||||
th:text="${#numbers.formatCurrency(transaction.fee)}">
|
||||
</td>
|
||||
<td class="numeric"
|
||||
th:text="${#numbers.formatCurrency(transaction.balance)}"></td>
|
||||
<td th:text="${transaction.date}"></td>
|
||||
<td th:text="${transaction.category}"></td>
|
||||
<td class="description" th:text="${transaction.description}"></td>
|
||||
|
||||
|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div th:fragment="tableInfo" class="row">
|
||||
<div class="col-md-6 align-self-center">
|
||||
<p id="dataTable_info" class="dataTables_info" role="status" aria-live="polite"
|
||||
th:text="'Showing ' + ${pageNum + 1} + ' to ' + ${pageSize} + ' of ' + ${transactionCount}">
|
||||
Showing 1 to 10 of 27</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<nav
|
||||
class="d-lg-flex justify-content-lg-end dataTables_paginate paging_simple_numbers">
|
||||
<nav class="mt-3">
|
||||
<ul class="pagination">
|
||||
<li th:classappend="${(pageNum + 1 == 1) ? 'disabled' : ''}"
|
||||
class="page-item">
|
||||
<a th:hx-get="@{/previousPage}" hx-trigger="click"
|
||||
hx-target="#dataTable" hx-swap="outerHtml" class="page-link btn"
|
||||
aria-label="Previous">
|
||||
<span aria-hidden="true">« Previous</span>
|
||||
</a>
|
||||
<li class="page-item" th:classappend="${pageNum == 0} ? 'disabled'">
|
||||
<button class="page-link" hx-get="/transactions-fragment"
|
||||
th:hx-vals="${'{"pageNum":' + (pageNum - 1) + '}'}"
|
||||
hx-include="#searchValue, #itemSize" hx-target="#table-container">«
|
||||
Previous</button>
|
||||
</li>
|
||||
<li th:classappend="${(pageNum == pageSize - 1) ? 'disabled' : ''}"
|
||||
class="page-item">
|
||||
<a th:hx-get="@{/nextPage}" hx-trigger="click" hx-target="#dataTable"
|
||||
hx-swap="outerHtml" class="page-link btn" aria-label="Next">
|
||||
<span aria-hidden="true">Next »</span></a>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Page [[${pageNum + 1}]] of [[${totalPages}]]</span>
|
||||
</li>
|
||||
<li class="page-item"
|
||||
th:classappend="${pageNum + 1 >= totalPages} ? 'disabled'">
|
||||
<button class="page-link" hx-get="/transactions-fragment"
|
||||
th:hx-vals="${'{"pageNum":' + (pageNum + 1) + '}'}"
|
||||
hx-include="#searchValue, #itemSize" hx-target="#table-container">Next
|
||||
»</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
@ -205,14 +174,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Footer-->
|
||||
<div th:replace="~{fragments/footer :: footer}"></div>
|
||||
</div>
|
||||
|
||||
<a class="border rounded d-inline scroll-to-top" href="#page-top">
|
||||
<span class="material-icons">keyboard_arrow_up</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue