diff --git a/src/main/java/com/expensys/controller/ExpenseManagementController.java b/src/main/java/com/expensys/controller/ExpenseManagementController.java index 4dcf280..7eade0d 100644 --- a/src/main/java/com/expensys/controller/ExpenseManagementController.java +++ b/src/main/java/com/expensys/controller/ExpenseManagementController.java @@ -40,8 +40,21 @@ public ResponseEntity> getReport(@ModelAttribute ReportReque } @GetMapping("/{month}") - public ResponseEntity> getExpenseByMonth(@PathVariable Month month) { - return new ResponseEntity<>(expenseManagementService.getExpensesByMonth(month), HttpStatus.OK); + public ResponseEntity> getExpenseByMonth( + @PathVariable Month month, + @RequestParam(name = "page", required = false) Integer page, + @RequestParam(name = "itemsPerPage", required = false) Integer itemsPerPage, + @RequestParam(name = "sortField", required = false) String sortField, + @RequestParam(name = "sortOrder", required = false) String sortOrder + ) { + try { + // Update the service method to handle new parameters + List expenses = expenseManagementService.getExpensesByMonth(month, page, itemsPerPage, sortField, sortOrder); + + return new ResponseEntity<>(expenses, HttpStatus.OK); + } catch (RuntimeException e) { + throw e; + } } @GetMapping diff --git a/src/main/java/com/expensys/controller/ExpensysController.java b/src/main/java/com/expensys/controller/ExpensysController.java index fd0d310..ee40942 100644 --- a/src/main/java/com/expensys/controller/ExpensysController.java +++ b/src/main/java/com/expensys/controller/ExpensysController.java @@ -13,4 +13,8 @@ public String index() { public String addExpense(){ return "addexpense"; } + @GetMapping("/expenses") + public String expensesByMonth(){ + return "showExpenseByMonth"; + } } diff --git a/src/main/java/com/expensys/controller/ReportController.java b/src/main/java/com/expensys/controller/ReportController.java deleted file mode 100644 index 8b78f0d..0000000 --- a/src/main/java/com/expensys/controller/ReportController.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.expensys.controller; - -import com.expensys.service.main.ExpenseManagementService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.RestController; - -@RestController -//@RequestMapping("/report") -public class ReportController { - Logger logger = LoggerFactory.getLogger(ReportController.class); - private final ExpenseManagementService expenseManagementService; - - public ReportController(ExpenseManagementService expenseManagementService) { - this.expenseManagementService = expenseManagementService; - } - -} diff --git a/src/main/java/com/expensys/service/ExpenseService.java b/src/main/java/com/expensys/service/ExpenseService.java index 934ba53..d01b75a 100644 --- a/src/main/java/com/expensys/service/ExpenseService.java +++ b/src/main/java/com/expensys/service/ExpenseService.java @@ -56,9 +56,6 @@ private List prepareExpenseListFromExpenseEntityList(List {}",expense); -// } return expenseList; } diff --git a/src/main/java/com/expensys/service/MainCategoryReportService.java b/src/main/java/com/expensys/service/MainCategoryReportService.java index e170835..12783ff 100644 --- a/src/main/java/com/expensys/service/MainCategoryReportService.java +++ b/src/main/java/com/expensys/service/MainCategoryReportService.java @@ -4,10 +4,8 @@ import com.expensys.model.Expense; import com.expensys.model.request.ReportRequest; import com.expensys.model.response.MonthlyReport; -import com.expensys.repository.CategoryMappingRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @@ -17,16 +15,9 @@ @Service public class MainCategoryReportService implements ICategoryReportService { private static final Logger logger = LoggerFactory.getLogger(MainCategoryReportService.class); - private final CategoryMappingRepository categoryMappingRepository; - - @Autowired - public MainCategoryReportService(CategoryMappingRepository categoryMappingRepository) { - this.categoryMappingRepository = categoryMappingRepository; - } @Override public List prepareReport(ReportRequest reportRequest, List expenseList) { - expenseList = expenseList.stream().filter(expense -> MAIN_CATEGORIES.contains(expense.getCategory())).toList(); return ExpenseToReportConvertor.getInstance().prepareReportListFromExpenseList(expenseList, reportRequest); } diff --git a/src/main/java/com/expensys/service/SubCategoryReportService.java b/src/main/java/com/expensys/service/SubCategoryReportService.java index 58ff8b3..2748e49 100644 --- a/src/main/java/com/expensys/service/SubCategoryReportService.java +++ b/src/main/java/com/expensys/service/SubCategoryReportService.java @@ -4,10 +4,8 @@ import com.expensys.model.Expense; import com.expensys.model.request.ReportRequest; import com.expensys.model.response.MonthlyReport; -import com.expensys.repository.CategoryMappingRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @@ -17,12 +15,6 @@ @Service public class SubCategoryReportService implements ICategoryReportService{ private static final Logger logger = LoggerFactory.getLogger(SubCategoryReportService.class); - private final CategoryMappingRepository categoryMappingRepository; - - @Autowired - public SubCategoryReportService(CategoryMappingRepository categoryMappingRepository) { - this.categoryMappingRepository = categoryMappingRepository; - } @Override public List prepareReport(ReportRequest reportRequest, List expenseList) { diff --git a/src/main/java/com/expensys/service/main/ExpenseManagementService.java b/src/main/java/com/expensys/service/main/ExpenseManagementService.java index f50dd1c..999b2be 100644 --- a/src/main/java/com/expensys/service/main/ExpenseManagementService.java +++ b/src/main/java/com/expensys/service/main/ExpenseManagementService.java @@ -15,6 +15,9 @@ import java.time.LocalDate; import java.util.List; +import static java.util.Collections.reverseOrder; +import static java.util.Comparator.comparing; + @Service public class ExpenseManagementService { Logger logger = LoggerFactory.getLogger(ExpenseManagementService.class); @@ -44,8 +47,24 @@ public List getExpenseByDateRange(LocalDate startDate, LocalDate return expenseService.getExpenseByDateRange(startDate, endDate); } - public List getExpensesByMonth(Month month){ - return expenseService.getExpenseEntitiesByMonth(month); + public List getExpensesByMonth(Month month, Integer page, Integer itemsPerPage, String sortField, String sortOrder){ + List sortedExpenses = expenseService.getExpenseEntitiesByMonth(month) + .stream() + .sorted(comparing(ExpenseEntity::getDate, reverseOrder()) + .thenComparing(ExpenseEntity::getId, reverseOrder())) + .toList(); + logger.info("sortedExpenses -> {}",sortedExpenses); + + if(page == null || itemsPerPage == null) return sortedExpenses; + + int startIndex = (page-1) * itemsPerPage; + int endIndex = startIndex+itemsPerPage; + + if(startIndex > sortedExpenses.size() || endIndex > sortedExpenses.size()){ + throw new RuntimeException("No data available for pageNo: "+page); + } + return sortedExpenses.subList(startIndex, endIndex); + } } diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css new file mode 100644 index 0000000..0b917dd --- /dev/null +++ b/src/main/resources/static/css/styles.css @@ -0,0 +1,232 @@ +/* CSS Reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* Global styling */ +body { + font-family: 'Arial', sans-serif; + margin: 0; + padding: 0; + background: #ecf0f1; /* Clouds background */ + color: #333; /* Dark gray text */ + transition: background-color 0.3s ease-in-out; +} + +/* Headings */ +h1, h2 { + text-align: center; +} + +h1 { + font-size: 48px; + margin-top: 20px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); + color: #3498db; /* Dodger blue */ + transition: color 0.3s ease-in-out; +} + +h2 { + font-size: 30px; + margin: 0; + color: #3498db; /* Dodger blue */ +} + +/* Form elements */ +section#filter-options { + background-color: #fff; + padding: 10px 0; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); + text-align: center; +} + +label { + font-weight: bold; +} + +select { + font-size: 14px; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + background: #f2f2f2; +} + +select:hover { + border-color: #2980b9; /* Darker blue on hover */ +} + +/* Additional Elements */ +#loadingSpinner { + display: block; + margin: 20px auto; + border: 8px solid rgba(0, 0, 0, 0.1); + border-radius: 50%; + border-top: 8px solid #333; + width: 50px; + height: 50px; + animation: spin 1s linear infinite; +} + +#errorMessage { + display: block; + margin: 20px auto; + padding: 20px; + background-color: #e74c3c; /* Alizarin crimson */ + color: #fff; + border-radius: 10px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + text-align: center; +} + +/* Pagination controls */ +.pagination { + text-align: center; + margin: 20px 0; +} + +.pagination button { + background-color: #3498db; /* Dodger blue */ + color: #fff; + padding: 12px 24px; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + margin: 0 10px; + transition: background 0.3s ease-in-out; +} + +.pagination button:hover { + background-color: #2980b9; /* Darker blue on hover */ + transform: scale(1.05); /* Add scale effect on hover */ +} + +.current-page { + margin: 0 10px; + font-size: 18px; + font-weight: bold; + color: #3498db; /* Dodger blue */ + transition: color 0.3s ease-in-out; +} + +/* Tables */ +table { + width: 100%; + border-collapse: collapse; + margin: 50px auto; + background-color: #fff; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); + border-radius: 10px; + overflow: hidden; + transition: box-shadow 0.3s ease-in-out; + color: #333; /* Dark gray text */ +} + +th, td { + padding: 15px; + text-align: left; + border-bottom: 1px solid #ccc; + font-size: 18px; + transition: background-color 0.3s ease-in-out; +} + +th { + background-color: #3498db; /* Dodger blue */ + color: #fff; + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.1); +} + +tr:nth-child(even) { + background-color: #f9f9f9; /* Light gray background for even rows */ +} + +tr:hover { + background-color: #e0e0e0; /* Slightly darker gray on hover */ +} + +/* Sorting icons */ +.sort-icon { + display: inline-block; + margin-left: 5px; + font-size: 16px; + transition: transform 0.3s ease-in-out; +} + +.sortable:hover .sort-icon { + transform: scale(1.2); +} + +/* Pagination controls at the top and bottom */ +.pagination-controls { + text-align: center; + margin: 20px 0; +} + +.pagination-controls button { + background-color: #3498db; /* Dodger blue */ + color: #fff; + padding: 12px 24px; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + margin: 0 10px; + transition: background 0.3s ease-in-out; +} + +.pagination-controls button:hover { + background-color: #2980b9; /* Darker blue on hover */ + transform: scale(1.05); /* Add scale effect on hover */ +} + +.pagination-controls .current-page { + margin: 0 10px; + font-size: 18px; + font-weight: bold; + color: #3498db; /* Dodger blue */ + transition: color 0.3s ease-in-out; +} + +/* Responsive styles */ +@media only screen and (max-width: 768px) { + select { + width: 100%; + max-width: calc(100% - 40px); + } + + table { + max-width: calc(100% - 40px); + overflow-x: auto; + } + + th, td { + font-size: 14px; + word-break: break-all; + padding: 15px; + text-align: left; + } + + h1 { + font-size: 36px; + } + + h2 { + font-size: 26px; + } + + .pagination-controls button { + padding: 10px 20px; + font-size: 14px; + margin: 0 5px; + } +} + +/* Loading Spinner Animation */ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/src/main/resources/static/js/expenseByMonth.js b/src/main/resources/static/js/expenseByMonth.js new file mode 100644 index 0000000..d8fae8e --- /dev/null +++ b/src/main/resources/static/js/expenseByMonth.js @@ -0,0 +1,150 @@ +let currentPage = 1; +const itemsPerPage = 10; // Adjust as needed +let currentSortField = null; +let currentSortOrder = 1; +let data = []; // Declare data at a higher scope + +const monthSelect = document.getElementById('monthSelect'); +const tbody = document.querySelector('#expenseTable tbody'); +const loadingSpinner = document.getElementById('loadingSpinner'); +const errorMessage = document.getElementById('errorMessage'); + +function fetchExpenseData() { + const selectedMonth = monthSelect.value; + const apiUrl = `http://localhost:8080/expense/${selectedMonth}?page=${currentPage}&itemsPerPage=${itemsPerPage}&sortField=${currentSortField}&sortOrder=${currentSortOrder}`; + + loadingSpinner.innerHTML = 'Loading...'; + loadingSpinner.style.display = 'block'; + errorMessage.style.display = 'none'; + + const nextPageButtonTop = document.querySelector('#paginationTop button:last-of-type'); + const nextPageButtonBottom = document.querySelector('#paginationBottom button:last-of-type'); + + fetch(apiUrl) + .then(response => { + if (!response.ok) { + throw new Error(`API request failed with status: ${response.status}`); + } + return response.json(); + }) + .then(responseData => { + data = responseData; // Assign responseData to the data variable + tbody.innerHTML = ''; + + data.forEach(expense => { + const row = tbody.insertRow(-1); + ['date', 'item', 'category', 'spent', 'spentBy'].forEach((field, index) => { + const cell = row.insertCell(index); + if (field === 'spent') { + // Check if 'spent' is a number before formatting + const spentValue = typeof expense[field] === 'number' ? expense[field] : parseFloat(expense[field]); + if (!isNaN(spentValue)) { + const formattedSpent = spentValue.toFixed(2); + cell.textContent = formattedSpent; + } else { + cell.textContent = expense[field]; // If not a number, display original value + } + } else { + cell.textContent = expense[field]; + } + cell.setAttribute('data-label', field); + }); + }); + + + document.getElementById('currentPageTop').textContent = currentPage; + document.getElementById('currentPageBottom').textContent = currentPage; + + nextPageButtonTop.disabled = data.length < itemsPerPage; + nextPageButtonBottom.disabled = data.length < itemsPerPage; + + if (data.length >= itemsPerPage) { + updateCurrentPageElement(); + } else { + // No more items on the current page + nextPageButtonTop.disabled = true; + nextPageButtonBottom.disabled = true; + } + }) + .catch(error => { + console.error('Error fetching data:', error); + errorMessage.innerHTML = `Error fetching data. Please try again.
Error: ${error.message}`; + errorMessage.style.display = 'block'; + + // No more items on the current page + nextPageButtonTop.disabled = true; + nextPageButtonBottom.disabled = true; + }) + .finally(() => { + loadingSpinner.style.display = 'none'; + }); +} + +function sortColumn(field) { + if (currentSortField === field) { + currentSortOrder *= -1; + } else { + currentSortField = field; + currentSortOrder = 1; + } + + document.querySelectorAll(".sort-icon").forEach(icon => (icon.textContent = '')); + + const icon = document.getElementById(`sort-${field}-icon`); + icon.textContent = currentSortOrder === 1 ? '▲' : '▼'; + + currentPage = 1; + fetchExpenseData(); + updateCurrentPageElement(); +} + +function nextPage() { + if (errorMessage.style.display === 'none') { + currentPage++; + fetchExpenseData(); + } else { + console.error('Fix the error before going to the next page.'); + } +} + +function previousPage() { + if (currentPage > 1) { + currentPage--; + fetchExpenseData(); + updateCurrentPageElement(); + } +} + +function updateCurrentPageElement() { + document.getElementById('currentPageTop').textContent = currentPage; + document.getElementById('currentPageBottom').textContent = currentPage; +} + +document.addEventListener('DOMContentLoaded', () => { + const months = [ + "January", "February", "March", "April", + "May", "June", "July", "August", + "September", "October", "November", "December" + ]; + + months.forEach((month, index) => { + const option = document.createElement('option'); + option.value = month.toUpperCase(); + option.textContent = month; + monthSelect.appendChild(option); + }); + + const savedMonth = localStorage.getItem('selectedMonth'); + + if (savedMonth) { + monthSelect.value = savedMonth; + } + + fetchExpenseData(); + updateCurrentPageElement(); +}); + +monthSelect.addEventListener('change', () => { + const selectedMonth = monthSelect.value; + localStorage.setItem('selectedMonth', selectedMonth); +}); \ No newline at end of file diff --git a/src/main/resources/static/js/reportScript.js b/src/main/resources/static/js/reportScript.js index 0bd40d6..c43146f 100644 --- a/src/main/resources/static/js/reportScript.js +++ b/src/main/resources/static/js/reportScript.js @@ -40,9 +40,10 @@ function createReportCard(data, selectedSpentBy) { const reportCard = document.createElement("div"); reportCard.className = "report-card"; + const formattedTotalSpendings = parseFloat(data.totalSpendings).toFixed(2); reportCard.innerHTML = `

${data.month}

-

Total Spendings: ₹${data.totalSpendings}

+

Total Spendings: ₹${formattedTotalSpendings}

@@ -67,7 +68,7 @@ ${sortReportData(data.reportInfo, selectedSpentBy).map(info => ` - + ${selectedSpentBy === "ALL" ? "" : ``} `).join('')} diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index e7a1f90..189aad7 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -6,7 +6,7 @@ Monthly Spending Report - + diff --git a/src/main/resources/templates/showExpenseByMonth.html b/src/main/resources/templates/showExpenseByMonth.html new file mode 100644 index 0000000..a9748d5 --- /dev/null +++ b/src/main/resources/templates/showExpenseByMonth.html @@ -0,0 +1,58 @@ + + + + + + Expense Tracker + + + +

Expense Tracker

+ + + + +
+ + +
+ + +
+ + 1 + +
+ +
${info.subCategory}₹${info.spent}₹${parseFloat(info.spent).toFixed(2)}${info.spentBy}
+ + + + + + + + + + +
Date + + Item + + Category + + Spent + + Spent By + +
+ + +
+ + 1 + +
+ + + \ No newline at end of file