Compare commits
No commits in common. "0c248adff7b985a0c929efdfd63cae7db372a4b0" and "40b1966f1462bd69ee40829c2ccf8b49a24ee86a" have entirely different histories.
0c248adff7
...
40b1966f14
7 changed files with 29 additions and 279 deletions
27
README.md
27
README.md
|
@ -4,9 +4,6 @@
|
|||
|
||||
Munera is a companion for managing expenses efficiently and effortlessly, whether you're tracking daily expenditures, managing recurring expenses, or keeping tabs on creditors and debtors.
|
||||
|
||||
![Dashboard](src/main/resources/pictures/dashboard.png)
|
||||
![Grid](src/main/resources/pictures/grid.png)
|
||||
|
||||
### 1. Expense Management
|
||||
|
||||
- Create, read, update, and delete expenses with the following details:
|
||||
|
@ -39,15 +36,14 @@ Munera is a companion for managing expenses efficiently and effortlessly, whethe
|
|||
|
||||
1. **Filtering and Sorting**
|
||||
- Keep implementing sorting on more columns, start implementing filtering
|
||||
- Specification
|
||||
|
||||
2. **Weekly and Monthly Summaries**
|
||||
- Create functionality to generate weekly and monthly summaries, including filtering and sorting options.
|
||||
- ~~Create a dashboard or log of "next expenses" that lists the next recurring expenses that you expect to receive~~
|
||||
~~- Create a dashboard or log of "next expenses" that lists the next recurring expenses that you expect to receive~~
|
||||
|
||||
3. **Reports for Creditors and Debtors**
|
||||
- ~~Develop reports outlining debts or credits for each creditor and debtor to provide users with a comprehensive overview.~~
|
||||
- ~~CRUD operations for creditors and debtors~~
|
||||
~~- Develop reports outlining debts or credits for each creditor and debtor to provide users with a comprehensive overview.~~
|
||||
~~- CRUD operations for creditors and debtors~~
|
||||
|
||||
4. **Create a way to set the currency of each expense**
|
||||
- It should be possible in the form of an expense
|
||||
|
@ -55,21 +51,12 @@ Munera is a companion for managing expenses efficiently and effortlessly, whethe
|
|||
- If possible, the calculations should take into account the different currencies
|
||||
|
||||
5. **Events**
|
||||
- ~~Options to create events in which to put expenses (vacations, congress, etc)~~
|
||||
- ~~Each event has a number of people connected to it, expenses can be added to these people~~
|
||||
~~- Options to create events in which to put expenses (vacations, congress, etc)~~
|
||||
~~- Each event has a number of people connected to it, expenses can be added to these people~~
|
||||
- A reports tells which people need to give/take money to which people in order to be even
|
||||
|
||||
6. **Misc**
|
||||
- PeriodUnit and Interval need to be implemented with a scheduler
|
||||
- Graphs could use more work
|
||||
- More validation in form
|
||||
- Login page
|
||||
- Migration tool for DB changes in prod
|
||||
|
||||
## Known Issues
|
||||
|
||||
- Form still needs more validation when empty, some entities can be created with all null values, even the ones that have constraints throw SQL errors, they need to be gracefully handled.
|
||||
- Errors need to be caught and handled
|
||||
- Graphs still need ~~a lot of~~ **some** improvements
|
||||
- PeriodUnit and Interval need to be implemented with a scheduler
|
||||
- ~~ExpenseView dosent refresh after an edit operation anymore~~
|
||||
- Graphs still need a lot of improvements
|
||||
- PeriodUnit and Interval need to be implemented
|
|
@ -28,5 +28,4 @@ public interface ExpenseRepository extends JpaRepository<Expense, Long>, JpaSpec
|
|||
Set<Expense> findUnpaidDebtorsExpensesByPersonId(@Param("personId") Long personId);
|
||||
|
||||
boolean existsByIdAndIsResolvedTrue(Long id);
|
||||
|
||||
List<Expense> findAllByOrderByDateDesc();}
|
||||
}
|
|
@ -70,8 +70,4 @@ public class ExpenseService {
|
|||
return this.repository.existsByIdAndIsResolvedTrue(expense.getId());
|
||||
}
|
||||
|
||||
public List<Expense> findAllOrderByDateDescending() {
|
||||
return this.repository.findAllByOrderByDateDesc();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,108 +1,48 @@
|
|||
package com.application.munera.views.expenses;
|
||||
|
||||
import com.application.munera.data.Expense;
|
||||
import com.application.munera.data.Person;
|
||||
import com.application.munera.services.ExpenseService;
|
||||
import com.application.munera.services.PersonService;
|
||||
import com.application.munera.views.MainLayout;
|
||||
import com.nimbusds.jose.shaded.gson.Gson;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Year;
|
||||
import java.time.YearMonth;
|
||||
import java.time.format.TextStyle;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
//@HtmlImport("frontend://styles/shared-styles.html") // If you have custom styles
|
||||
@PageTitle("Dashboard")
|
||||
@Route(value = "dashboard", layout = MainLayout.class)
|
||||
@Route(value = "highcharts-view", layout = MainLayout.class)
|
||||
public class DashboardView extends Div {
|
||||
|
||||
private final ExpenseService expenseService;
|
||||
private final PersonService personService;
|
||||
|
||||
public DashboardView(final ExpenseService expenseService, final PersonService personService) {
|
||||
public DashboardView(final ExpenseService expenseService) {
|
||||
this.expenseService = expenseService;
|
||||
this.personService = personService;
|
||||
addClassName("highcharts-view"); // Optional CSS class for styling
|
||||
|
||||
VerticalLayout mainLayout = new VerticalLayout();
|
||||
mainLayout.setSizeFull();
|
||||
mainLayout.getStyle().set("padding", "10px"); // Add padding to main layout
|
||||
VerticalLayout layout = new VerticalLayout();
|
||||
layout.setSizeFull();
|
||||
|
||||
// Create a horizontal layout for the top row
|
||||
HorizontalLayout topRowLayout = new HorizontalLayout();
|
||||
topRowLayout.setSizeFull();
|
||||
topRowLayout.setHeight("50%"); // Make sure the top row occupies half of the page height
|
||||
topRowLayout.getStyle().set("padding", "10px"); // Add padding to top row
|
||||
// Create a div to host the chart
|
||||
Div chartDiv = new Div();
|
||||
chartDiv.setId("chart"); // Assign an ID to this div for later reference
|
||||
chartDiv.getStyle().set("min-height", "400px"); // Set minimum height for the chart
|
||||
layout.add(chartDiv);
|
||||
add(layout);
|
||||
|
||||
// Create and add the existing bar chart to the top left
|
||||
Div barChartDiv = new Div();
|
||||
barChartDiv.setId("barChart");
|
||||
barChartDiv.getStyle().set("min-height", "100%"); // Ensure it occupies the full height of the container
|
||||
barChartDiv.getStyle().set("width", "50%"); // Occupy half of the width
|
||||
barChartDiv.getStyle().set("border", "1px solid #ccc"); // Add border
|
||||
barChartDiv.getStyle().set("padding", "10px"); // Add padding inside the border
|
||||
topRowLayout.add(barChartDiv);
|
||||
String jsInit = generateChartInitializationScript();
|
||||
|
||||
// Create and add the new pie chart to the top right
|
||||
Div pieChartDiv = new Div();
|
||||
pieChartDiv.setId("pieChart");
|
||||
pieChartDiv.getStyle().set("min-height", "100%"); // Ensure it occupies the full height of the container
|
||||
pieChartDiv.getStyle().set("width", "50%"); // Occupy half of the width
|
||||
pieChartDiv.getStyle().set("border", "1px solid #ccc"); // Add border
|
||||
pieChartDiv.getStyle().set("padding", "10px"); // Add padding inside the border
|
||||
topRowLayout.add(pieChartDiv);
|
||||
|
||||
mainLayout.add(topRowLayout);
|
||||
|
||||
// Create a horizontal layout for the bottom row
|
||||
HorizontalLayout bottomRowLayout = new HorizontalLayout();
|
||||
bottomRowLayout.setSizeFull();
|
||||
bottomRowLayout.setHeight("50%"); // Make sure the bottom row occupies the other half of the page height
|
||||
bottomRowLayout.getStyle().set("padding", "10px"); // Add padding to bottom row
|
||||
|
||||
// Create the bottom left chart
|
||||
Div bottomLeftChartDiv = new Div();
|
||||
bottomLeftChartDiv.setId("bottomLeftChart");
|
||||
bottomLeftChartDiv.getStyle().set("min-height", "100%"); // Ensure it occupies the full height of the container
|
||||
bottomLeftChartDiv.getStyle().set("width", "50%"); // Occupy half of the width
|
||||
bottomLeftChartDiv.getStyle().set("border", "1px solid #ccc"); // Add border
|
||||
bottomLeftChartDiv.getStyle().set("padding", "10px"); // Add padding inside the border
|
||||
bottomRowLayout.add(bottomLeftChartDiv);
|
||||
|
||||
// Placeholder for the bottom right chart
|
||||
Div bottomRightChartDiv = new Div();
|
||||
bottomRightChartDiv.setId("bottomRightChart");
|
||||
bottomRightChartDiv.getStyle().set("min-height", "100%"); // Ensure it occupies the full height of the container
|
||||
bottomRightChartDiv.getStyle().set("width", "50%"); // Occupy half of the width
|
||||
bottomRightChartDiv.getStyle().set("border", "1px solid #ccc"); // Add border
|
||||
bottomRightChartDiv.getStyle().set("padding", "10px"); // Add padding inside the border
|
||||
bottomRowLayout.add(bottomRightChartDiv);
|
||||
|
||||
mainLayout.add(bottomRowLayout);
|
||||
|
||||
add(mainLayout);
|
||||
|
||||
String barChartJs = generateBarChartScript();
|
||||
String pieChartJs = generatePieChartScript();
|
||||
String bottomLeftChartJs = generateNegativeColumnChartScript();
|
||||
String bottomRightChartJs = generateExpensesOverTimeByCategoryScript();
|
||||
|
||||
// Execute the JavaScript to initialize the charts
|
||||
getElement().executeJs(barChartJs);
|
||||
getElement().executeJs(pieChartJs);
|
||||
getElement().executeJs(bottomLeftChartJs);
|
||||
getElement().executeJs(bottomRightChartJs);
|
||||
// Execute the JavaScript to initialize the chart
|
||||
getElement().executeJs(jsInit);
|
||||
}
|
||||
|
||||
private String generateBarChartScript() {
|
||||
private String generateChartInitializationScript() {
|
||||
List<Expense> expenses = expenseService.findAllByYear(Year.now().getValue());
|
||||
|
||||
// Prepare data for Highcharts
|
||||
|
@ -118,6 +58,7 @@ public class DashboardView extends Div {
|
|||
// Populate map with actual data
|
||||
for (Expense expense : expenses) {
|
||||
String monthName = expense.getDate().getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH);
|
||||
// Convert BigDecimal to Double
|
||||
Double amount = expense.getCost().doubleValue();
|
||||
monthlyData.put(monthName, monthlyData.get(monthName) + amount);
|
||||
}
|
||||
|
@ -130,7 +71,7 @@ public class DashboardView extends Div {
|
|||
data.setCharAt(data.length() - 1, ']'); // Replace last comma with closing bracket
|
||||
|
||||
// Generate JavaScript initialization
|
||||
return "Highcharts.chart('barChart', {" +
|
||||
return "Highcharts.chart('chart', {" +
|
||||
"chart: {" +
|
||||
"type: 'column'" +
|
||||
"}," +
|
||||
|
@ -146,176 +87,4 @@ public class DashboardView extends Div {
|
|||
"}]" +
|
||||
"});";
|
||||
}
|
||||
|
||||
private String generatePieChartScript() {
|
||||
List<Expense> expenses = expenseService.findAllByYear(Year.now().getValue());
|
||||
|
||||
// Group expenses by category name and sum their costs
|
||||
Map<String, Double> categoryData = expenses.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
expense -> expense.getCategory().getName(),
|
||||
LinkedHashMap::new,
|
||||
Collectors.summingDouble(expense -> expense.getCost().doubleValue())
|
||||
));
|
||||
|
||||
// Prepare series data for Highcharts
|
||||
StringBuilder data = new StringBuilder("[");
|
||||
for (Map.Entry<String, Double> entry : categoryData.entrySet()) {
|
||||
data.append("{ name: '").append(entry.getKey()).append("', y: ").append(entry.getValue()).append(" },");
|
||||
}
|
||||
data.setCharAt(data.length() - 1, ']'); // Replace last comma with closing bracket
|
||||
|
||||
// Generate JavaScript initialization
|
||||
return "Highcharts.chart('pieChart', {" +
|
||||
"chart: {" +
|
||||
"type: 'pie'" +
|
||||
"}," +
|
||||
"title: {" +
|
||||
"text: 'Expenses by Category for " + Year.now().getValue() + "'" +
|
||||
"}," +
|
||||
"plotOptions: {" +
|
||||
"pie: {" +
|
||||
"size: '80%'" + // Adjust size to make the pie chart larger
|
||||
"}" +
|
||||
"}," +
|
||||
"series: [{" +
|
||||
"name: 'Expenses'," +
|
||||
"colorByPoint: true," +
|
||||
"data: " + data + // Use the data fetched from DB
|
||||
"}]" +
|
||||
"});";
|
||||
}
|
||||
|
||||
private String generateNegativeColumnChartScript() {
|
||||
final var people = personService.findAll().stream()
|
||||
.filter(person -> personService.calculateNetBalance(person).compareTo(BigDecimal.ZERO) != 0)
|
||||
.toList();
|
||||
if (people.isEmpty()) return generatePlaceholderChartScript("bottomLeftChart", "No Data Available");
|
||||
|
||||
Map<String, Double> personData = new LinkedHashMap<>();
|
||||
|
||||
for (Person person : people) {
|
||||
BigDecimal balance = personService.calculateNetBalance(person);
|
||||
personData.put(person.getFirstName(), balance.doubleValue());
|
||||
}
|
||||
|
||||
// Prepare series data for Highcharts with conditional coloring
|
||||
StringBuilder data = new StringBuilder("[");
|
||||
for (Map.Entry<String, Double> entry : personData.entrySet()) {
|
||||
double value = entry.getValue();
|
||||
String color = value >= 0 ? "#90EE90" : "#FF9999"; // Green for positive, red for negative
|
||||
data.append("{ y: ").append(value).append(", color: '").append(color).append("' },");
|
||||
}
|
||||
data.setCharAt(data.length() - 1, ']'); // Replace last comma with closing bracket
|
||||
|
||||
// Generate JavaScript initialization
|
||||
return "Highcharts.chart('bottomLeftChart', {" +
|
||||
"chart: {" +
|
||||
"type: 'column'," + // Specify the chart type as column
|
||||
"}," +
|
||||
"title: {" +
|
||||
"text: 'Net Balances by Person'" +
|
||||
"}," +
|
||||
"xAxis: {" +
|
||||
"categories: " + new Gson().toJson(personData.keySet()) + // Categories are the person names
|
||||
"}," +
|
||||
"yAxis: {" +
|
||||
"title: {" +
|
||||
"text: 'Balance'" +
|
||||
"}," +
|
||||
"plotLines: [{" +
|
||||
"value: 0," +
|
||||
"width: 1," +
|
||||
"color: '#808080'" +
|
||||
"}]" +
|
||||
"}," +
|
||||
"plotOptions: {" + // Add plotOptions to configure the column width
|
||||
"column: {" +
|
||||
"pointWidth: 50" + // Adjust the width of the columns (in pixels)
|
||||
"}" +
|
||||
"}," +
|
||||
"series: [{" +
|
||||
"name: 'Balance'," +
|
||||
"data: " + data + // Use the data fetched from DB
|
||||
"}]" +
|
||||
"});"; }
|
||||
|
||||
private String generatePlaceholderChartScript(String divId, String title) {
|
||||
return "Highcharts.chart('" + divId + "', {" +
|
||||
"chart: {" +
|
||||
"type: 'column'" +
|
||||
"}," +
|
||||
"title: {" +
|
||||
"text: '" + title + "'" +
|
||||
"}," +
|
||||
"series: [{" +
|
||||
"name: 'Data'," +
|
||||
"data: [0]" + // Placeholder data
|
||||
"}]" +
|
||||
"});";
|
||||
}
|
||||
|
||||
private String generateExpensesOverTimeByCategoryScript() {
|
||||
List<Expense> expenses = expenseService.findAllByYear(Year.now().getValue());
|
||||
|
||||
// Group expenses by category and by month
|
||||
Map<String, Map<String, Double>> categoryMonthlyData = new LinkedHashMap<>();
|
||||
|
||||
YearMonth currentYearMonth = YearMonth.now().withMonth(1); // Start from January
|
||||
List<String> monthNames = new ArrayList<>();
|
||||
|
||||
for (int i = 1; i <= 12; i++) {
|
||||
String monthName = currentYearMonth.getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH);
|
||||
monthNames.add(monthName);
|
||||
currentYearMonth = currentYearMonth.plusMonths(1); // Move to the next month
|
||||
}
|
||||
|
||||
for (Expense expense : expenses) {
|
||||
String categoryName = expense.getCategory().getName();
|
||||
String monthName = expense.getDate().getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH);
|
||||
Double amount = expense.getCost().doubleValue();
|
||||
|
||||
categoryMonthlyData.putIfAbsent(categoryName, new LinkedHashMap<>());
|
||||
Map<String, Double> monthlyData = categoryMonthlyData.get(categoryName);
|
||||
|
||||
// Initialize all months to 0 for each category
|
||||
for (String month : monthNames) {
|
||||
monthlyData.putIfAbsent(month, 0.0);
|
||||
}
|
||||
|
||||
monthlyData.put(monthName, monthlyData.get(monthName) + amount);
|
||||
}
|
||||
|
||||
// Prepare series data for Highcharts
|
||||
StringBuilder seriesData = new StringBuilder("[");
|
||||
for (Map.Entry<String, Map<String, Double>> entry : categoryMonthlyData.entrySet()) {
|
||||
String categoryName = entry.getKey();
|
||||
Map<String, Double> monthlyData = entry.getValue();
|
||||
|
||||
seriesData.append("{");
|
||||
seriesData.append("name: '").append(categoryName).append("',");
|
||||
seriesData.append("data: ").append(monthlyData.values());
|
||||
seriesData.append("},");
|
||||
}
|
||||
seriesData.setCharAt(seriesData.length() - 1, ']'); // Replace last comma with closing bracket
|
||||
|
||||
// Generate JavaScript initialization
|
||||
return "Highcharts.chart('bottomRightChart', {" +
|
||||
"chart: {" +
|
||||
"type: 'line'" +
|
||||
"}," +
|
||||
"title: {" +
|
||||
"text: 'Expenses Over Time by Category for " + Year.now().getValue() + "'" +
|
||||
"}," +
|
||||
"xAxis: {" +
|
||||
"categories: " + new Gson().toJson(monthNames) +
|
||||
"}," +
|
||||
"yAxis: {" +
|
||||
"title: {" +
|
||||
"text: 'Amount'" +
|
||||
"}" +
|
||||
"}," +
|
||||
"series: " + seriesData +
|
||||
"});";
|
||||
}
|
||||
}
|
|
@ -99,7 +99,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
|||
grid.addColumn(new ComponentRenderer<>(expense1 -> createBadge(expenseService.isExpenseResolved(expense1)))).setHeader("Status").setSortable(true);
|
||||
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
||||
|
||||
grid.setItems(this.expenseService.findAllOrderByDateDescending());
|
||||
grid.setItems(this.expenseService.findAll());
|
||||
grid.setPaginatorSize(5);
|
||||
grid.setPageSize(25); // setting page size
|
||||
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
||||
|
@ -276,7 +276,6 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
|||
}
|
||||
|
||||
private void refreshGrid() {
|
||||
grid.setItems(this.expenseService.findAllOrderByDateDescending());
|
||||
grid.select(null);
|
||||
grid.getDataProvider().refreshAll();
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 77 KiB |
Binary file not shown.
Before Width: | Height: | Size: 177 KiB |
Loading…
Reference in a new issue