Compare commits

..

10 commits

Author SHA1 Message Date
filippo-ferrari
0c248adff7 docs: README.md 2024-07-15 19:49:21 +02:00
filippo-ferrari
7f27ff0b3d fix: order after refresh 2024-07-15 19:44:11 +02:00
filippo-ferrari
435ddd65a2 feat: added default sorting to grid 2024-07-15 19:38:43 +02:00
filippo-ferrari
3a2c1ff782 feat: added new graph to DashboardView 2024-07-15 19:38:17 +02:00
filippo-ferrari
a52927f779 docs: README.md 2024-07-14 14:58:23 +02:00
filippo-ferrari
ea79738ea7 docs: README.md 2024-07-14 14:57:38 +02:00
filippo-ferrari
ce4c9b3a06 fix: size of graph colums 2024-07-14 14:49:34 +02:00
filippo-ferrari
a2efef0ccf feat: new graph added in DashboardView 2024-07-14 14:45:55 +02:00
filippo-ferrari
7c01f5d5cc feat: new graph added in DashboardView 2024-07-14 14:26:37 +02:00
filippo-ferrari
1b0b469c7b fix: refresh grid issue 2024-07-13 18:44:13 +02:00
7 changed files with 279 additions and 29 deletions

View file

@ -4,6 +4,9 @@
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. 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 ### 1. Expense Management
- Create, read, update, and delete expenses with the following details: - Create, read, update, and delete expenses with the following details:
@ -36,14 +39,15 @@ Munera is a companion for managing expenses efficiently and effortlessly, whethe
1. **Filtering and Sorting** 1. **Filtering and Sorting**
- Keep implementing sorting on more columns, start implementing filtering - Keep implementing sorting on more columns, start implementing filtering
- Specification
2. **Weekly and Monthly Summaries** 2. **Weekly and Monthly Summaries**
- Create functionality to generate weekly and monthly summaries, including filtering and sorting options. - 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** 3. **Reports for Creditors and Debtors**
~~- Develop reports outlining debts or credits for each creditor and debtor to provide users with a comprehensive overview.~~ - ~~Develop reports outlining debts or credits for each creditor and debtor to provide users with a comprehensive overview.~~
~~- CRUD operations for creditors and debtors~~ - ~~CRUD operations for creditors and debtors~~
4. **Create a way to set the currency of each expense** 4. **Create a way to set the currency of each expense**
- It should be possible in the form of an expense - It should be possible in the form of an expense
@ -51,12 +55,21 @@ Munera is a companion for managing expenses efficiently and effortlessly, whethe
- If possible, the calculations should take into account the different currencies - If possible, the calculations should take into account the different currencies
5. **Events** 5. **Events**
~~- Options to create events in which to put expenses (vacations, congress, etc)~~ - ~~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~~ - ~~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 - 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 ## 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. - 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.
- Graphs still need a lot of improvements - Errors need to be caught and handled
- PeriodUnit and Interval need to be implemented - 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~~

View file

@ -28,4 +28,5 @@ public interface ExpenseRepository extends JpaRepository<Expense, Long>, JpaSpec
Set<Expense> findUnpaidDebtorsExpensesByPersonId(@Param("personId") Long personId); Set<Expense> findUnpaidDebtorsExpensesByPersonId(@Param("personId") Long personId);
boolean existsByIdAndIsResolvedTrue(Long id); boolean existsByIdAndIsResolvedTrue(Long id);
}
List<Expense> findAllByOrderByDateDesc();}

View file

@ -70,4 +70,8 @@ public class ExpenseService {
return this.repository.existsByIdAndIsResolvedTrue(expense.getId()); return this.repository.existsByIdAndIsResolvedTrue(expense.getId());
} }
public List<Expense> findAllOrderByDateDescending() {
return this.repository.findAllByOrderByDateDesc();
}
} }

View file

@ -1,48 +1,108 @@
package com.application.munera.views.expenses; package com.application.munera.views.expenses;
import com.application.munera.data.Expense; import com.application.munera.data.Expense;
import com.application.munera.data.Person;
import com.application.munera.services.ExpenseService; import com.application.munera.services.ExpenseService;
import com.application.munera.services.PersonService;
import com.application.munera.views.MainLayout; import com.application.munera.views.MainLayout;
import com.nimbusds.jose.shaded.gson.Gson; import com.nimbusds.jose.shaded.gson.Gson;
import com.vaadin.flow.component.html.Div; 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.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import java.math.BigDecimal;
import java.time.Year; import java.time.Year;
import java.time.YearMonth; import java.time.YearMonth;
import java.time.format.TextStyle; import java.time.format.TextStyle;
import java.util.LinkedHashMap; import java.util.*;
import java.util.List; import java.util.stream.Collectors;
import java.util.Locale;
import java.util.Map;
//@HtmlImport("frontend://styles/shared-styles.html") // If you have custom styles //@HtmlImport("frontend://styles/shared-styles.html") // If you have custom styles
@Route(value = "highcharts-view", layout = MainLayout.class) @PageTitle("Dashboard")
@Route(value = "dashboard", layout = MainLayout.class)
public class DashboardView extends Div { public class DashboardView extends Div {
private final ExpenseService expenseService; private final ExpenseService expenseService;
private final PersonService personService;
public DashboardView(final ExpenseService expenseService) { public DashboardView(final ExpenseService expenseService, final PersonService personService) {
this.expenseService = expenseService; this.expenseService = expenseService;
this.personService = personService;
addClassName("highcharts-view"); // Optional CSS class for styling addClassName("highcharts-view"); // Optional CSS class for styling
VerticalLayout layout = new VerticalLayout(); VerticalLayout mainLayout = new VerticalLayout();
layout.setSizeFull(); mainLayout.setSizeFull();
mainLayout.getStyle().set("padding", "10px"); // Add padding to main layout
// Create a div to host the chart // Create a horizontal layout for the top row
Div chartDiv = new Div(); HorizontalLayout topRowLayout = new HorizontalLayout();
chartDiv.setId("chart"); // Assign an ID to this div for later reference topRowLayout.setSizeFull();
chartDiv.getStyle().set("min-height", "400px"); // Set minimum height for the chart topRowLayout.setHeight("50%"); // Make sure the top row occupies half of the page height
layout.add(chartDiv); topRowLayout.getStyle().set("padding", "10px"); // Add padding to top row
add(layout);
String jsInit = generateChartInitializationScript(); // 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);
// Execute the JavaScript to initialize the chart // Create and add the new pie chart to the top right
getElement().executeJs(jsInit); 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);
} }
private String generateChartInitializationScript() { private String generateBarChartScript() {
List<Expense> expenses = expenseService.findAllByYear(Year.now().getValue()); List<Expense> expenses = expenseService.findAllByYear(Year.now().getValue());
// Prepare data for Highcharts // Prepare data for Highcharts
@ -58,7 +118,6 @@ public class DashboardView extends Div {
// Populate map with actual data // Populate map with actual data
for (Expense expense : expenses) { for (Expense expense : expenses) {
String monthName = expense.getDate().getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH); String monthName = expense.getDate().getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH);
// Convert BigDecimal to Double
Double amount = expense.getCost().doubleValue(); Double amount = expense.getCost().doubleValue();
monthlyData.put(monthName, monthlyData.get(monthName) + amount); monthlyData.put(monthName, monthlyData.get(monthName) + amount);
} }
@ -71,7 +130,7 @@ public class DashboardView extends Div {
data.setCharAt(data.length() - 1, ']'); // Replace last comma with closing bracket data.setCharAt(data.length() - 1, ']'); // Replace last comma with closing bracket
// Generate JavaScript initialization // Generate JavaScript initialization
return "Highcharts.chart('chart', {" + return "Highcharts.chart('barChart', {" +
"chart: {" + "chart: {" +
"type: 'column'" + "type: 'column'" +
"}," + "}," +
@ -87,4 +146,176 @@ 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 +
"});";
}
} }

View file

@ -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.addColumn(new ComponentRenderer<>(expense1 -> createBadge(expenseService.isExpenseResolved(expense1)))).setHeader("Status").setSortable(true);
grid.getColumns().forEach(col -> col.setAutoWidth(true)); grid.getColumns().forEach(col -> col.setAutoWidth(true));
grid.setItems(this.expenseService.findAll()); grid.setItems(this.expenseService.findAllOrderByDateDescending());
grid.setPaginatorSize(5); grid.setPaginatorSize(5);
grid.setPageSize(25); // setting page size grid.setPageSize(25); // setting page size
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
@ -276,6 +276,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
} }
private void refreshGrid() { private void refreshGrid() {
grid.setItems(this.expenseService.findAllOrderByDateDescending());
grid.select(null); grid.select(null);
grid.getDataProvider().refreshAll(); grid.getDataProvider().refreshAll();
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB