Add /categories-cvs
This commit is contained in:
parent
31f9272409
commit
3e978a831c
15 changed files with 365 additions and 43 deletions
18
.classpath
18
.classpath
|
|
@ -2,7 +2,6 @@
|
||||||
<classpath>
|
<classpath>
|
||||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="module" value="true"/>
|
|
||||||
<attribute name="maven.pomderived" value="true"/>
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
</attributes>
|
</attributes>
|
||||||
</classpathentry>
|
</classpathentry>
|
||||||
|
|
@ -38,5 +37,22 @@
|
||||||
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
|
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
|
||||||
</attributes>
|
</attributes>
|
||||||
</classpathentry>
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" path="target/generated-sources/annotations">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="optional" value="true"/>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
<attribute name="ignore_optional_problems" value="true"/>
|
||||||
|
<attribute name="m2e-apt" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="optional" value="true"/>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
<attribute name="ignore_optional_problems" value="true"/>
|
||||||
|
<attribute name="m2e-apt" value="true"/>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
<classpathentry kind="output" path="target/classes"/>
|
<classpathentry kind="output" path="target/classes"/>
|
||||||
</classpath>
|
</classpath>
|
||||||
|
|
|
||||||
2
.settings/org.eclipse.jdt.apt.core.prefs
Normal file
2
.settings/org.eclipse.jdt.apt.core.prefs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
org.eclipse.jdt.apt.aptEnabled=false
|
||||||
|
|
@ -7,5 +7,6 @@ org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
|
||||||
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
||||||
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
|
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
|
||||||
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
|
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
|
||||||
org.eclipse.jdt.core.compiler.release=enabled
|
org.eclipse.jdt.core.compiler.processAnnotations=disabled
|
||||||
|
org.eclipse.jdt.core.compiler.release=disabled
|
||||||
org.eclipse.jdt.core.compiler.source=17
|
org.eclipse.jdt.core.compiler.source=17
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
|
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
|
||||||
|
|
||||||
<wb-module deploy-name="budget.web-0.0.1-SNAPSHOT">
|
<wb-module deploy-name="budget.web-0.0.1-SNAPSHOT">
|
||||||
|
|
||||||
<wb-resource deploy-path="/" source-path="/target/m2e-wtp/web-resources"/>
|
<wb-resource deploy-path="/" source-path="/target/m2e-wtp/web-resources"/>
|
||||||
|
|
||||||
<wb-resource deploy-path="/" source-path="/src/main/webapp"/>
|
<wb-resource deploy-path="/" source-path="/src/main/webapp"/>
|
||||||
|
|
||||||
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/>
|
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/>
|
||||||
|
|
||||||
<wb-resource deploy-path="/" source-path="/WebContent" tag="defaultRootSource"/>
|
<wb-resource deploy-path="/" source-path="/WebContent" tag="defaultRootSource"/>
|
||||||
|
|
||||||
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/resources"/>
|
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/resources"/>
|
||||||
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/test/java"/>
|
<wb-resource deploy-path="/WEB-INF/classes" source-path="/target/generated-sources/annotations"/>
|
||||||
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/test/resources"/>
|
<wb-resource deploy-path="/WEB-INF/classes" source-path="/target/generated-test-sources/test-annotations"/>
|
||||||
|
|
||||||
<property name="java-output-path" value="/com.stephenschafer.budget.web/build/classes"/>
|
<property name="java-output-path" value="/com.stephenschafer.budget.web/build/classes"/>
|
||||||
|
|
||||||
<property name="context-root" value="budget.web"/>
|
<property name="context-root" value="budget.web"/>
|
||||||
|
|
||||||
</wb-module>
|
</wb-module>
|
||||||
|
|
||||||
</project-modules>
|
</project-modules>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@
|
||||||
<fixed facet="java"/>
|
<fixed facet="java"/>
|
||||||
<fixed facet="wst.jsdt.web"/>
|
<fixed facet="wst.jsdt.web"/>
|
||||||
<installed facet="wst.jsdt.web" version="1.0"/>
|
<installed facet="wst.jsdt.web" version="1.0"/>
|
||||||
<installed facet="jst.web" version="4.0"/>
|
|
||||||
<installed facet="java" version="17"/>
|
<installed facet="java" version="17"/>
|
||||||
|
<installed facet="jst.web" version="2.5"/>
|
||||||
</faceted-project>
|
</faceted-project>
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,15 @@
|
||||||
<url-pattern>/categories</url-pattern>
|
<url-pattern>/categories</url-pattern>
|
||||||
</servlet-mapping>
|
</servlet-mapping>
|
||||||
|
|
||||||
|
<servlet>
|
||||||
|
<servlet-name>CategoriesCsvPage</servlet-name>
|
||||||
|
<servlet-class>com.stephenschafer.budget.web.CategoriesCsvPage</servlet-class>
|
||||||
|
<load-on-startup>1</load-on-startup>
|
||||||
|
</servlet>
|
||||||
|
|
||||||
|
<servlet-mapping>
|
||||||
|
<servlet-name>CategoriesCsvPage</servlet-name>
|
||||||
|
<url-pattern>/categories-csv</url-pattern>
|
||||||
|
</servlet-mapping>
|
||||||
|
|
||||||
</web-app>
|
</web-app>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
if mvn -f pom.xml clean install > build-jar.log 2> build-jar.err.log; then
|
if mvn -f pom.xml clean install > build-war.log 2> build-war.err.log; then
|
||||||
echo "success"
|
echo "success"
|
||||||
cp target/*.war /tmp
|
cp target/*.war /tmp
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
package com.stephenschafer.budget.web;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.naming.NamingException;
|
||||||
|
import javax.servlet.ServletConfig;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
public class CategoriesCsvPage extends HttpServlet {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(final ServletConfig config) throws ServletException {
|
||||||
|
try {
|
||||||
|
Configuration.INSTANCE.load();
|
||||||
|
}
|
||||||
|
catch (final Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
super.init(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
var paramYear = request.getParameterValues("year");
|
||||||
|
if (paramYear == null || paramYear.length == 0) {
|
||||||
|
final Calendar cal = new GregorianCalendar();
|
||||||
|
cal.setTime(new Date());
|
||||||
|
final var year = cal.get(Calendar.YEAR);
|
||||||
|
paramYear = new String[] { String.valueOf(year) };
|
||||||
|
}
|
||||||
|
var tmpIncludeExpenses = true;
|
||||||
|
var tmpIncludeIncome = true;
|
||||||
|
var tmpIncludePayments = false;
|
||||||
|
var tmpIncludeInvestments = false;
|
||||||
|
final var includes = request.getParameterValues("include");
|
||||||
|
if (includes != null && includes.length > 0) {
|
||||||
|
tmpIncludeExpenses = false;
|
||||||
|
tmpIncludeIncome = false;
|
||||||
|
tmpIncludePayments = false;
|
||||||
|
tmpIncludeInvestments = false;
|
||||||
|
for (final String include : includes) {
|
||||||
|
if ("expenses".equalsIgnoreCase(include)) {
|
||||||
|
tmpIncludeExpenses = true;
|
||||||
|
}
|
||||||
|
else if ("income".equalsIgnoreCase(include)) {
|
||||||
|
tmpIncludeIncome = true;
|
||||||
|
}
|
||||||
|
else if ("payments".equalsIgnoreCase(include)) {
|
||||||
|
tmpIncludePayments = true;
|
||||||
|
}
|
||||||
|
else if ("investments".equalsIgnoreCase(include)) {
|
||||||
|
tmpIncludeInvestments = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final var includeExpenses = tmpIncludeExpenses;
|
||||||
|
final var includeIncome = tmpIncludeIncome;
|
||||||
|
final var includePayments = tmpIncludePayments;
|
||||||
|
final var includeInvestments = tmpIncludeInvestments;
|
||||||
|
final var startDates = request.getParameterValues("startDate");
|
||||||
|
final var endDates = request.getParameterValues("endDate");
|
||||||
|
final var startDateString = startDates == null ? null
|
||||||
|
: startDates.length == 0 ? null : startDates[0];
|
||||||
|
final var endDateString = endDates == null ? null
|
||||||
|
: endDates.length == 0 ? null : endDates[0];
|
||||||
|
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
|
||||||
|
final Date startDate;
|
||||||
|
final Date endDate;
|
||||||
|
try {
|
||||||
|
startDate = startDateString == null ? null : df.parse(startDateString);
|
||||||
|
endDate = endDateString == null ? null : df.parse(endDateString);
|
||||||
|
}
|
||||||
|
catch (final ParseException e) {
|
||||||
|
response.sendError(400, "Invalid date");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Calendar startCal = new GregorianCalendar();
|
||||||
|
startCal.setTime(startDate);
|
||||||
|
final int startMonth = startCal.get(Calendar.MONTH);
|
||||||
|
final Calendar endCal = new GregorianCalendar();
|
||||||
|
endCal.setTime(endDate);
|
||||||
|
final int endMonth = endCal.get(Calendar.MONTH);
|
||||||
|
final int monthCount = endMonth - startMonth + 1;
|
||||||
|
final var out = response.getWriter();
|
||||||
|
response.setHeader("Content-Type", "text/csv");
|
||||||
|
try (var connection = Configuration.INSTANCE.getConnection()) {
|
||||||
|
for (final String year : paramYear) {
|
||||||
|
final var categoryMap = loadCategories(connection, year, includeIncome,
|
||||||
|
includePayments, includeInvestments, includeExpenses);
|
||||||
|
final var sql = Util.getResourceAsString("getTransactionDetail.sql") //
|
||||||
|
.replace("${databaseName}", "budget_" + year) //
|
||||||
|
.replace("${regexDatabaseName}", "budget");
|
||||||
|
try (var stmt = connection.prepareStatement(sql)) {
|
||||||
|
try (var rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
final var detail = new ReportDetail();
|
||||||
|
var i = 0;
|
||||||
|
detail.transactionId = rs.getInt(++i);
|
||||||
|
detail.date = rs.getDate(++i);
|
||||||
|
detail.source = rs.getString(++i);
|
||||||
|
detail.description = rs.getString(++i);
|
||||||
|
detail.amount = rs.getBigDecimal(++i);
|
||||||
|
if (rs.wasNull()) {
|
||||||
|
detail.amount = new BigDecimal(0);
|
||||||
|
}
|
||||||
|
final var categoryId = rs.getInt(++i);
|
||||||
|
detail.regex = rs.getString(++i);
|
||||||
|
detail.flags = rs.getInt(++i);
|
||||||
|
detail.requiredSource = rs.getString(++i);
|
||||||
|
detail.extraDescription = rs.getString(++i);
|
||||||
|
final var category = categoryMap.get(categoryId);
|
||||||
|
if (category != null && //
|
||||||
|
detail.isAfter(startDate) && //
|
||||||
|
detail.isBefore(endDate) && //
|
||||||
|
category.isIncluded()) {
|
||||||
|
category.addDetail(detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final var rootCategory = new ReportCategory(-1, null, "root");
|
||||||
|
for (final Integer categoryId : categoryMap.keySet()) {
|
||||||
|
final var category = categoryMap.get(categoryId);
|
||||||
|
if ((category != null) && (category.getParent() == null)) {
|
||||||
|
rootCategory.addChild(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.print(Util.getResourceAsString("categoryCsvHead.template"));
|
||||||
|
generateCategoryTable(out, rootCategory, 0, monthCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final SQLException | NamingException e) {
|
||||||
|
throw new ServletException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateCategoryTable(final PrintWriter out, final ReportCategory parent,
|
||||||
|
final int level, final int monthCount) throws IOException {
|
||||||
|
final var categoryGrandAverage = parent.getGrandAverage(monthCount);
|
||||||
|
final var categoryGrandAmount = parent.getGrandTotal();
|
||||||
|
var categoryCsvLine = Util.getResourceAsString("categoryCsv.template") //
|
||||||
|
.replace("${name}", parent.getQualifiedName()) //
|
||||||
|
.replace("${id}", parent.getId().toString()) //
|
||||||
|
.replace("${average}", categoryGrandAverage.toString()) //
|
||||||
|
.replace("${total}", categoryGrandAmount.toString()) //
|
||||||
|
.replace("${indent}", String.valueOf(level * 20) + "px");
|
||||||
|
parent.getLargestMonth();
|
||||||
|
for (int month = 0; month < 12; month++) {
|
||||||
|
categoryCsvLine = categoryCsvLine //
|
||||||
|
.replace("${total-" + month + "}", parent.getMonthGrandTotal(month).toString());
|
||||||
|
}
|
||||||
|
out.print(categoryCsvLine);
|
||||||
|
final var categories = parent.getChildCategories();
|
||||||
|
for (final ReportCategory category : categories) {
|
||||||
|
generateCategoryTable(out, category, level + 1, monthCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryFilter {
|
||||||
|
boolean include(ReportCategory category);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Integer, ReportCategory> loadCategories(final Connection connection, final String year,
|
||||||
|
final boolean includeIncome, final boolean includePayments,
|
||||||
|
final boolean includeInvestments, final boolean includeExpenses)
|
||||||
|
throws SQLException, IOException {
|
||||||
|
ReportCategory unknownCategory = null;
|
||||||
|
final Map<Integer, ReportCategory> categories = new HashMap<>();
|
||||||
|
final var sql = Util.getResourceAsString("getCategories.sql").replace("${databaseName}",
|
||||||
|
"budget");
|
||||||
|
try (var stmt = connection.prepareStatement(sql)) {
|
||||||
|
try (var rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
final Integer id = rs.getInt(1);
|
||||||
|
Integer parentId = rs.getInt(2);
|
||||||
|
if (rs.wasNull()) {
|
||||||
|
parentId = null;
|
||||||
|
}
|
||||||
|
final var name = rs.getString(3);
|
||||||
|
final var category = new ReportCategory(id, parentId, name);
|
||||||
|
categories.put(id, category);
|
||||||
|
if ("unknown".equals(name) && parentId == null) {
|
||||||
|
unknownCategory = category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final Integer categoryId : categories.keySet()) {
|
||||||
|
final var category = categories.get(categoryId);
|
||||||
|
category.updateParent(categories);
|
||||||
|
}
|
||||||
|
if (unknownCategory == null) {
|
||||||
|
unknownCategory = new ReportCategory(-1, null, "unknown");
|
||||||
|
categories.put(unknownCategory.getId(), unknownCategory);
|
||||||
|
}
|
||||||
|
for (final Integer categoryId : categories.keySet()) {
|
||||||
|
final var category = categories.get(categoryId);
|
||||||
|
final CategoryFilter filter = category1 -> {
|
||||||
|
final String categoryName = category1.getName();
|
||||||
|
if ("income".equals(categoryName) || "refunds".equals(categoryName)) {
|
||||||
|
if (!includeIncome) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ("payment".equals(categoryName)) {
|
||||||
|
if (!includePayments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ("investment".equals(categoryName)) {
|
||||||
|
if (!includeInvestments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!includeExpenses) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
final boolean included = filter.include(category);
|
||||||
|
category.setIncluded(included);
|
||||||
|
}
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.stephenschafer.budget.web;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
|
|
@ -94,6 +95,13 @@ public class CategoriesPage extends HttpServlet {
|
||||||
response.sendError(400, "Invalid date");
|
response.sendError(400, "Invalid date");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final Calendar startCal = new GregorianCalendar();
|
||||||
|
startCal.setTime(startDate);
|
||||||
|
final int startMonth = startCal.get(Calendar.MONTH);
|
||||||
|
final Calendar endCal = new GregorianCalendar();
|
||||||
|
endCal.setTime(endDate);
|
||||||
|
final int endMonth = endCal.get(Calendar.MONTH);
|
||||||
|
final int monthCount = endMonth - startMonth + 1;
|
||||||
final var out = response.getWriter();
|
final var out = response.getWriter();
|
||||||
response.setHeader("Content-Type", "text/html");
|
response.setHeader("Content-Type", "text/html");
|
||||||
try (var connection = Configuration.INSTANCE.getConnection()) {
|
try (var connection = Configuration.INSTANCE.getConnection()) {
|
||||||
|
|
@ -118,13 +126,16 @@ public class CategoriesPage extends HttpServlet {
|
||||||
try (var stmt = connection.prepareStatement(sql)) {
|
try (var stmt = connection.prepareStatement(sql)) {
|
||||||
try (var rs = stmt.executeQuery()) {
|
try (var rs = stmt.executeQuery()) {
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
final var detail = new Detail();
|
final var detail = new ReportDetail();
|
||||||
var i = 0;
|
var i = 0;
|
||||||
detail.transactionId = rs.getInt(++i);
|
detail.transactionId = rs.getInt(++i);
|
||||||
detail.date = rs.getDate(++i);
|
detail.date = rs.getDate(++i);
|
||||||
detail.source = rs.getString(++i);
|
detail.source = rs.getString(++i);
|
||||||
detail.description = rs.getString(++i);
|
detail.description = rs.getString(++i);
|
||||||
detail.amount = rs.getBigDecimal(++i);
|
detail.amount = rs.getBigDecimal(++i);
|
||||||
|
if (rs.wasNull()) {
|
||||||
|
detail.amount = new BigDecimal(0);
|
||||||
|
}
|
||||||
final var categoryId = rs.getInt(++i);
|
final var categoryId = rs.getInt(++i);
|
||||||
detail.regex = rs.getString(++i);
|
detail.regex = rs.getString(++i);
|
||||||
detail.flags = rs.getInt(++i);
|
detail.flags = rs.getInt(++i);
|
||||||
|
|
@ -140,7 +151,7 @@ public class CategoriesPage extends HttpServlet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final var rootCategory = new Category(-1, null, "root");
|
final var rootCategory = new ReportCategory(-1, null, "root");
|
||||||
for (final Integer categoryId : categoryMap.keySet()) {
|
for (final Integer categoryId : categoryMap.keySet()) {
|
||||||
final var category = categoryMap.get(categoryId);
|
final var category = categoryMap.get(categoryId);
|
||||||
if ((category != null) && (category.getParent() == null)) {
|
if ((category != null) && (category.getParent() == null)) {
|
||||||
|
|
@ -155,7 +166,7 @@ public class CategoriesPage extends HttpServlet {
|
||||||
out.println(grandHead);
|
out.println(grandHead);
|
||||||
out.println("</thead>");
|
out.println("</thead>");
|
||||||
out.println("<tbody>");
|
out.println("<tbody>");
|
||||||
generateCategoryTable(out, rootCategory, 0);
|
generateCategoryTable(out, rootCategory, 0, monthCount);
|
||||||
out.println("</tbody>");
|
out.println("</tbody>");
|
||||||
out.println("</table>");
|
out.println("</table>");
|
||||||
out.println("</div>");
|
out.println("</div>");
|
||||||
|
|
@ -168,15 +179,15 @@ public class CategoriesPage extends HttpServlet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void generateCategoryTable(final PrintWriter out, final Category parent,
|
private void generateCategoryTable(final PrintWriter out, final ReportCategory parent,
|
||||||
final int level) throws IOException {
|
final int level, final int monthCount) throws IOException {
|
||||||
final var details = parent.getDetails();
|
final var details = parent.getDetails();
|
||||||
final var categoryAmount = parent.getDetailTotal();
|
final var categoryGrandAverage = parent.getGrandAverage(monthCount);
|
||||||
final var categoryGrandAmount = parent.getGrandTotal();
|
final var categoryGrandAmount = parent.getGrandTotal();
|
||||||
var categoryHeadHtml = Util.getResourceAsString("categoryHead.html") //
|
var categoryHeadHtml = Util.getResourceAsString("categoryHead.html") //
|
||||||
.replace("${name}", parent.getName()) //
|
.replace("${name}", parent.getName()) //
|
||||||
.replace("${id}", parent.getId().toString()) //
|
.replace("${id}", parent.getId().toString()) //
|
||||||
.replace("${amount}", categoryAmount.toString()) //
|
.replace("${average}", categoryGrandAverage.toString()) //
|
||||||
.replace("${total}", categoryGrandAmount.toString()) //
|
.replace("${total}", categoryGrandAmount.toString()) //
|
||||||
.replace("${indent}", String.valueOf(level * 20) + "px");
|
.replace("${indent}", String.valueOf(level * 20) + "px");
|
||||||
final int largestMonth = parent.getLargestMonth();
|
final int largestMonth = parent.getLargestMonth();
|
||||||
|
|
@ -195,7 +206,7 @@ public class CategoriesPage extends HttpServlet {
|
||||||
out.print(categoryDetailHeadHtml);
|
out.print(categoryDetailHeadHtml);
|
||||||
}
|
}
|
||||||
Collections.sort(details);
|
Collections.sort(details);
|
||||||
for (final Detail detail : details) {
|
for (final ReportDetail detail : details) {
|
||||||
final var categoryDetailHtml = Util.getResourceAsString("categoryDetail.html") //
|
final var categoryDetailHtml = Util.getResourceAsString("categoryDetail.html") //
|
||||||
.replace("${source}", detail.source) //
|
.replace("${source}", detail.source) //
|
||||||
.replace("${description}", detail.description) //
|
.replace("${description}", detail.description) //
|
||||||
|
|
@ -214,8 +225,8 @@ public class CategoriesPage extends HttpServlet {
|
||||||
out.println("</td>");
|
out.println("</td>");
|
||||||
out.println("</tr>");
|
out.println("</tr>");
|
||||||
final var categories = parent.getChildCategories();
|
final var categories = parent.getChildCategories();
|
||||||
for (final Category category : categories) {
|
for (final ReportCategory category : categories) {
|
||||||
generateCategoryTable(out, category, level + 1);
|
generateCategoryTable(out, category, level + 1, monthCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,15 +263,15 @@ public class CategoriesPage extends HttpServlet {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CategoryFilter {
|
interface CategoryFilter {
|
||||||
boolean include(Category category);
|
boolean include(ReportCategory category);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Integer, Category> loadCategories(final Connection connection, final String year,
|
private Map<Integer, ReportCategory> loadCategories(final Connection connection, final String year,
|
||||||
final boolean includeIncome, final boolean includePayments,
|
final boolean includeIncome, final boolean includePayments,
|
||||||
final boolean includeInvestments, final boolean includeExpenses)
|
final boolean includeInvestments, final boolean includeExpenses)
|
||||||
throws SQLException, IOException {
|
throws SQLException, IOException {
|
||||||
Category unknownCategory = null;
|
ReportCategory unknownCategory = null;
|
||||||
final Map<Integer, Category> categories = new HashMap<>();
|
final Map<Integer, ReportCategory> categories = new HashMap<>();
|
||||||
final var sql = Util.getResourceAsString("getCategories.sql").replace("${databaseName}",
|
final var sql = Util.getResourceAsString("getCategories.sql").replace("${databaseName}",
|
||||||
"budget");
|
"budget");
|
||||||
try (var stmt = connection.prepareStatement(sql)) {
|
try (var stmt = connection.prepareStatement(sql)) {
|
||||||
|
|
@ -272,7 +283,7 @@ public class CategoriesPage extends HttpServlet {
|
||||||
parentId = null;
|
parentId = null;
|
||||||
}
|
}
|
||||||
final var name = rs.getString(3);
|
final var name = rs.getString(3);
|
||||||
final var category = new Category(id, parentId, name);
|
final var category = new ReportCategory(id, parentId, name);
|
||||||
categories.put(id, category);
|
categories.put(id, category);
|
||||||
if ("unknown".equals(name) && parentId == null) {
|
if ("unknown".equals(name) && parentId == null) {
|
||||||
unknownCategory = category;
|
unknownCategory = category;
|
||||||
|
|
@ -285,7 +296,7 @@ public class CategoriesPage extends HttpServlet {
|
||||||
category.updateParent(categories);
|
category.updateParent(categories);
|
||||||
}
|
}
|
||||||
if (unknownCategory == null) {
|
if (unknownCategory == null) {
|
||||||
unknownCategory = new Category(-1, null, "unknown");
|
unknownCategory = new ReportCategory(-1, null, "unknown");
|
||||||
categories.put(unknownCategory.getId(), unknownCategory);
|
categories.put(unknownCategory.getId(), unknownCategory);
|
||||||
}
|
}
|
||||||
for (final Integer categoryId : categories.keySet()) {
|
for (final Integer categoryId : categories.keySet()) {
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,33 @@
|
||||||
package com.stephenschafer.budget.web;
|
package com.stephenschafer.budget.web;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class Category {
|
public class ReportCategory {
|
||||||
private final Integer id;
|
private final Integer id;
|
||||||
private final Integer parentId;
|
private final Integer parentId;
|
||||||
private final String name;
|
private final String name;
|
||||||
private Category parent;
|
private ReportCategory parent;
|
||||||
private boolean included;
|
private boolean included;
|
||||||
private List<Detail> details = new ArrayList<>();
|
private List<ReportDetail> details = new ArrayList<>();
|
||||||
private final Map<Integer, Category> children = new HashMap<>();
|
private Set<Integer> months = null;
|
||||||
|
private final Map<Integer, ReportCategory> children = new HashMap<>();
|
||||||
|
|
||||||
public Category(final Integer id, final Integer parentId, final String name) {
|
public ReportCategory(final Integer id, final Integer parentId, final String name) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.parentId = parentId;
|
this.parentId = parentId;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateParent(final Map<Integer, Category> categories) {
|
public void updateParent(final Map<Integer, ReportCategory> categories) {
|
||||||
if (parentId == null) {
|
if (parentId == null) {
|
||||||
parent = null;
|
parent = null;
|
||||||
}
|
}
|
||||||
|
|
@ -33,15 +37,15 @@ public class Category {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addChild(final Category category) {
|
public void addChild(final ReportCategory category) {
|
||||||
children.put(category.id, category);
|
children.put(category.id, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Category getParent() {
|
public ReportCategory getParent() {
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setParent(final Category parent) {
|
public void setParent(final ReportCategory parent) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,25 +71,25 @@ public class Category {
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Detail> getDetails() {
|
public List<ReportDetail> getDetails() {
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDetails(final List<Detail> details) {
|
public void setDetails(final List<ReportDetail> details) {
|
||||||
this.details = details;
|
this.details = details;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addDetail(final Detail detail) {
|
public void addDetail(final ReportDetail detail) {
|
||||||
details.add(detail);
|
details.add(detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<Integer, Category> getChildren() {
|
public Map<Integer, ReportCategory> getChildren() {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BigDecimal getDetailTotal() {
|
public BigDecimal getDetailTotal() {
|
||||||
var amount = new BigDecimal(0);
|
var amount = new BigDecimal(0);
|
||||||
for (final Detail detail : details) {
|
for (final ReportDetail detail : details) {
|
||||||
amount = amount.add(detail.amount);
|
amount = amount.add(detail.amount);
|
||||||
}
|
}
|
||||||
return amount;
|
return amount;
|
||||||
|
|
@ -93,7 +97,7 @@ public class Category {
|
||||||
|
|
||||||
public BigDecimal getMonthTotal(final int month) {
|
public BigDecimal getMonthTotal(final int month) {
|
||||||
var amount = new BigDecimal(0);
|
var amount = new BigDecimal(0);
|
||||||
for (final Detail detail : details) {
|
for (final ReportDetail detail : details) {
|
||||||
final Calendar cal = new GregorianCalendar();
|
final Calendar cal = new GregorianCalendar();
|
||||||
cal.setTime(detail.date);
|
cal.setTime(detail.date);
|
||||||
if (month == cal.get(Calendar.MONTH)) {
|
if (month == cal.get(Calendar.MONTH)) {
|
||||||
|
|
@ -103,6 +107,21 @@ public class Category {
|
||||||
return amount;
|
return amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<Integer> getMonths() {
|
||||||
|
Set<Integer> months = this.months;
|
||||||
|
if (months == null) {
|
||||||
|
months = new HashSet<>();
|
||||||
|
for (final ReportDetail detail : details) {
|
||||||
|
final Calendar cal = new GregorianCalendar();
|
||||||
|
cal.setTime(detail.date);
|
||||||
|
final int month = cal.get(Calendar.MONTH);
|
||||||
|
months.add(Integer.valueOf(month));
|
||||||
|
}
|
||||||
|
this.months = months;
|
||||||
|
}
|
||||||
|
return months;
|
||||||
|
}
|
||||||
|
|
||||||
public BigDecimal getGrandTotal() {
|
public BigDecimal getGrandTotal() {
|
||||||
var total = getDetailTotal();
|
var total = getDetailTotal();
|
||||||
for (final Integer categoryId : children.keySet()) {
|
for (final Integer categoryId : children.keySet()) {
|
||||||
|
|
@ -112,6 +131,14 @@ public class Category {
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigDecimal getGrandAverage(final int monthCount) {
|
||||||
|
final BigDecimal grandTotal = getGrandTotal();
|
||||||
|
if (monthCount == 0) {
|
||||||
|
return new BigDecimal(0);
|
||||||
|
}
|
||||||
|
return grandTotal.divide(new BigDecimal(monthCount), RoundingMode.HALF_DOWN);
|
||||||
|
}
|
||||||
|
|
||||||
public BigDecimal getMonthGrandTotal(final int month) {
|
public BigDecimal getMonthGrandTotal(final int month) {
|
||||||
var total = getMonthTotal(month);
|
var total = getMonthTotal(month);
|
||||||
for (final Integer categoryId : children.keySet()) {
|
for (final Integer categoryId : children.keySet()) {
|
||||||
|
|
@ -139,8 +166,8 @@ public class Category {
|
||||||
return maxCount > 1 ? -1 : maxMonth;
|
return maxCount > 1 ? -1 : maxMonth;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Category> getChildCategories() {
|
public List<ReportCategory> getChildCategories() {
|
||||||
final List<Category> categories = new ArrayList<>();
|
final List<ReportCategory> categories = new ArrayList<>();
|
||||||
for (final Integer categoryId : children.keySet()) {
|
for (final Integer categoryId : children.keySet()) {
|
||||||
final var category = children.get(categoryId);
|
final var category = children.get(categoryId);
|
||||||
categories.add(category);
|
categories.add(category);
|
||||||
|
|
@ -3,7 +3,7 @@ package com.stephenschafer.budget.web;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
class Detail implements Comparable<Detail> {
|
class ReportDetail implements Comparable<ReportDetail> {
|
||||||
int transactionId;
|
int transactionId;
|
||||||
String source;
|
String source;
|
||||||
String description;
|
String description;
|
||||||
|
|
@ -15,7 +15,7 @@ class Detail implements Comparable<Detail> {
|
||||||
String extraDescription;
|
String extraDescription;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(final Detail arg1) {
|
public int compareTo(final ReportDetail arg1) {
|
||||||
final var arg0 = this;
|
final var arg0 = this;
|
||||||
int comparison;
|
int comparison;
|
||||||
if (arg0.date == null) {
|
if (arg0.date == null) {
|
||||||
1
src/main/resources/categoryCsv.template
Normal file
1
src/main/resources/categoryCsv.template
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
${name},${average}, ${total}, ${total-0}, ${total-1}, ${total-2}, ${total-3}, ${total-4}, ${total-5}, ${total-6}, ${total-7}, ${total-8}, ${total-9}, ${total-10}, ${total-11}
|
||||||
1
src/main/resources/categoryCsvHead.template
Normal file
1
src/main/resources/categoryCsvHead.template
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Category, Average, Total, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<h2 style="cursor: pointer; font-weight: normal;" onclick="showDetail(this, ${id})">${name}</h2>
|
<h2 style="cursor: pointer; font-weight: normal;" onclick="showDetail(this, ${id})">${name}</h2>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding-left: 15px; text-align: right; ">
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
<h2 style="font-weight: normal; ">${amount}</h2>
|
<h2 style="font-weight: normal; ">${average}</h2>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding-left: 15px; text-align: right; ">
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
<h2 style="font-weight: normal; ">${total}</h2>
|
<h2 style="font-weight: normal; ">${total}</h2>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th>Amount</th>
|
<th>Average</th>
|
||||||
<th>Total</th>
|
<th>Total</th>
|
||||||
<th>Jan</th>
|
<th>Jan</th>
|
||||||
<th>Feb</th>
|
<th>Feb</th>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue