Implement transaction list page and ensure consistency with budget list in sidebar
This commit is contained in:
parent
689dbc39e2
commit
d8b89bc8d2
9 changed files with 158 additions and 51 deletions
|
@ -1,5 +1,6 @@
|
||||||
package com.wbrawner.twigs.web
|
package com.wbrawner.twigs.web
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.service.budget.BudgetResponse
|
||||||
import com.wbrawner.twigs.service.user.UserResponse
|
import com.wbrawner.twigs.service.user.UserResponse
|
||||||
|
|
||||||
interface Page {
|
interface Page {
|
||||||
|
@ -9,8 +10,18 @@ interface Page {
|
||||||
|
|
||||||
interface AuthenticatedPage : Page {
|
interface AuthenticatedPage : Page {
|
||||||
val user: UserResponse
|
val user: UserResponse
|
||||||
|
val budgets: List<BudgetListItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class BudgetListItem(val id: String, val name: String, val description: String, val selected: Boolean)
|
||||||
|
|
||||||
|
fun BudgetResponse.toBudgetListItem(selectedId: String? = null) = BudgetListItem(
|
||||||
|
id = id,
|
||||||
|
name = name.orEmpty(),
|
||||||
|
description = description.orEmpty(),
|
||||||
|
selected = id == selectedId
|
||||||
|
)
|
||||||
|
|
||||||
object NotFoundPage : Page {
|
object NotFoundPage : Page {
|
||||||
override val title: String = "404 Not Found"
|
override val title: String = "404 Not Found"
|
||||||
override val error: String? = null
|
override val error: String? = null
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.wbrawner.twigs.web
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
|
import java.text.DateFormat
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -16,6 +17,8 @@ val decimalFormat = DecimalFormat.getNumberInstance(Locale.US).apply {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val shortDateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US)
|
||||||
|
|
||||||
|
|
||||||
fun Parameters.getAmount() = decimalFormat.parse(get("amount"))
|
fun Parameters.getAmount() = decimalFormat.parse(get("amount"))
|
||||||
?.toDouble()
|
?.toDouble()
|
||||||
|
|
|
@ -3,10 +3,11 @@ package com.wbrawner.twigs.web.budget
|
||||||
import com.wbrawner.twigs.service.budget.BudgetResponse
|
import com.wbrawner.twigs.service.budget.BudgetResponse
|
||||||
import com.wbrawner.twigs.service.user.UserResponse
|
import com.wbrawner.twigs.service.user.UserResponse
|
||||||
import com.wbrawner.twigs.web.AuthenticatedPage
|
import com.wbrawner.twigs.web.AuthenticatedPage
|
||||||
|
import com.wbrawner.twigs.web.BudgetListItem
|
||||||
import com.wbrawner.twigs.web.category.CategoryWithBalanceResponse
|
import com.wbrawner.twigs.web.category.CategoryWithBalanceResponse
|
||||||
|
|
||||||
data class BudgetListPage(
|
data class BudgetListPage(
|
||||||
val budgets: List<BudgetListItem>,
|
override val budgets: List<BudgetListItem>,
|
||||||
override val user: UserResponse,
|
override val user: UserResponse,
|
||||||
override val error: String? = null
|
override val error: String? = null
|
||||||
) : AuthenticatedPage {
|
) : AuthenticatedPage {
|
||||||
|
@ -14,7 +15,6 @@ data class BudgetListPage(
|
||||||
}
|
}
|
||||||
|
|
||||||
data class BudgetDetailsPage(
|
data class BudgetDetailsPage(
|
||||||
val budgets: List<BudgetListItem>,
|
|
||||||
val budget: BudgetResponse,
|
val budget: BudgetResponse,
|
||||||
val balances: BudgetBalances,
|
val balances: BudgetBalances,
|
||||||
val incomeCategories: List<CategoryWithBalanceResponse>,
|
val incomeCategories: List<CategoryWithBalanceResponse>,
|
||||||
|
@ -23,6 +23,7 @@ data class BudgetDetailsPage(
|
||||||
val archivedExpenseCategories: List<CategoryWithBalanceResponse>,
|
val archivedExpenseCategories: List<CategoryWithBalanceResponse>,
|
||||||
val transactionCount: String,
|
val transactionCount: String,
|
||||||
val monthAndYear: String,
|
val monthAndYear: String,
|
||||||
|
override val budgets: List<BudgetListItem>,
|
||||||
override val user: UserResponse,
|
override val user: UserResponse,
|
||||||
override val error: String? = null
|
override val error: String? = null
|
||||||
) : AuthenticatedPage {
|
) : AuthenticatedPage {
|
||||||
|
@ -31,6 +32,7 @@ data class BudgetDetailsPage(
|
||||||
|
|
||||||
data class BudgetFormPage(
|
data class BudgetFormPage(
|
||||||
val budget: BudgetResponse,
|
val budget: BudgetResponse,
|
||||||
|
override val budgets: List<BudgetListItem>,
|
||||||
override val user: UserResponse,
|
override val user: UserResponse,
|
||||||
override val error: String? = null
|
override val error: String? = null
|
||||||
) : AuthenticatedPage {
|
) : AuthenticatedPage {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.wbrawner.twigs.service.user.UserService
|
||||||
import com.wbrawner.twigs.toInstantOrNull
|
import com.wbrawner.twigs.toInstantOrNull
|
||||||
import com.wbrawner.twigs.web.NotFoundPage
|
import com.wbrawner.twigs.web.NotFoundPage
|
||||||
import com.wbrawner.twigs.web.category.CategoryWithBalanceResponse
|
import com.wbrawner.twigs.web.category.CategoryWithBalanceResponse
|
||||||
|
import com.wbrawner.twigs.web.toBudgetListItem
|
||||||
import com.wbrawner.twigs.web.user.TWIGS_SESSION_COOKIE
|
import com.wbrawner.twigs.web.user.TWIGS_SESSION_COOKIE
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
@ -52,13 +53,14 @@ fun Application.budgetWebRoutes(
|
||||||
MustacheContent(
|
MustacheContent(
|
||||||
"budget-form.mustache",
|
"budget-form.mustache",
|
||||||
BudgetFormPage(
|
BudgetFormPage(
|
||||||
BudgetResponse(
|
budget = BudgetResponse(
|
||||||
id = "",
|
id = "",
|
||||||
name = "",
|
name = "",
|
||||||
description = "",
|
description = "",
|
||||||
users = listOf()
|
users = listOf()
|
||||||
),
|
),
|
||||||
user
|
budgets = budgetService.budgetsForUser(user.id).map { it.toBudgetListItem() },
|
||||||
|
user = user
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -76,14 +78,15 @@ fun Application.budgetWebRoutes(
|
||||||
MustacheContent(
|
MustacheContent(
|
||||||
"budget-form.mustache",
|
"budget-form.mustache",
|
||||||
BudgetFormPage(
|
BudgetFormPage(
|
||||||
BudgetResponse(
|
budget = BudgetResponse(
|
||||||
id = "",
|
id = "",
|
||||||
name = call.parameters["name"].orEmpty(),
|
name = call.parameters["name"].orEmpty(),
|
||||||
description = call.parameters["description"].orEmpty(),
|
description = call.parameters["description"].orEmpty(),
|
||||||
users = listOf()
|
users = listOf()
|
||||||
),
|
),
|
||||||
user,
|
budgets = budgetService.budgetsForUser(user.id).map { it.toBudgetListItem() },
|
||||||
e.message
|
user = user,
|
||||||
|
error = e.message
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -150,16 +153,16 @@ fun Application.budgetWebRoutes(
|
||||||
call.respond(
|
call.respond(
|
||||||
MustacheContent(
|
MustacheContent(
|
||||||
"budget-details.mustache", BudgetDetailsPage(
|
"budget-details.mustache", BudgetDetailsPage(
|
||||||
budgets = budgets.map { it.toBudgetListItem(budgetId) }.sortedBy { it.name },
|
|
||||||
budget = budget,
|
budget = budget,
|
||||||
balances = balances,
|
balances = balances,
|
||||||
incomeCategories = incomeCategories,
|
incomeCategories = incomeCategories,
|
||||||
archivedIncomeCategories = archivedIncomeCategories,
|
|
||||||
expenseCategories = expenseCategories,
|
expenseCategories = expenseCategories,
|
||||||
|
archivedIncomeCategories = archivedIncomeCategories,
|
||||||
archivedExpenseCategories = archivedExpenseCategories,
|
archivedExpenseCategories = archivedExpenseCategories,
|
||||||
transactionCount = NumberFormat.getNumberInstance(Locale.US)
|
transactionCount = NumberFormat.getNumberInstance(Locale.US)
|
||||||
.format(transactions.size),
|
.format(transactions.size),
|
||||||
monthAndYear = YearMonth.now().format(DateTimeFormatter.ofPattern("MMMM yyyy")),
|
monthAndYear = YearMonth.now().format(DateTimeFormatter.ofPattern("MMMM yyyy")),
|
||||||
|
budgets = budgets.map { it.toBudgetListItem(budgetId) }.sortedBy { it.name },
|
||||||
user = user
|
user = user
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -176,7 +179,12 @@ fun Application.budgetWebRoutes(
|
||||||
call.respond(
|
call.respond(
|
||||||
MustacheContent(
|
MustacheContent(
|
||||||
"budget-form.mustache",
|
"budget-form.mustache",
|
||||||
BudgetFormPage(budget, user)
|
BudgetFormPage(
|
||||||
|
budget = budget,
|
||||||
|
budgets = budgetService.budgetsForUser(user.id)
|
||||||
|
.map { it.toBudgetListItem(budget.id) },
|
||||||
|
user = user
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -210,15 +218,6 @@ data class BudgetBalances(
|
||||||
val maxProgressBarValue: Long = maxOf(expectedExpenses, expectedIncome, actualIncome, actualExpenses)
|
val maxProgressBarValue: Long = maxOf(expectedExpenses, expectedIncome, actualIncome, actualExpenses)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class BudgetListItem(val id: String, val name: String, val description: String, val selected: Boolean)
|
|
||||||
|
|
||||||
private fun BudgetResponse.toBudgetListItem(selectedId: String? = null) = BudgetListItem(
|
|
||||||
id = id,
|
|
||||||
name = name.orEmpty(),
|
|
||||||
description = description.orEmpty(),
|
|
||||||
selected = id == selectedId
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun Parameters.toBudgetRequest() = BudgetRequest(
|
private fun Parameters.toBudgetRequest() = BudgetRequest(
|
||||||
name = get("name"),
|
name = get("name"),
|
||||||
description = get("description"),
|
description = get("description"),
|
||||||
|
|
|
@ -5,15 +5,16 @@ import com.wbrawner.twigs.service.category.CategoryResponse
|
||||||
import com.wbrawner.twigs.service.transaction.TransactionResponse
|
import com.wbrawner.twigs.service.transaction.TransactionResponse
|
||||||
import com.wbrawner.twigs.service.user.UserResponse
|
import com.wbrawner.twigs.service.user.UserResponse
|
||||||
import com.wbrawner.twigs.web.AuthenticatedPage
|
import com.wbrawner.twigs.web.AuthenticatedPage
|
||||||
|
import com.wbrawner.twigs.web.BudgetListItem
|
||||||
import com.wbrawner.twigs.web.budget.toCurrencyString
|
import com.wbrawner.twigs.web.budget.toCurrencyString
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
|
|
||||||
data class CategoryDetailsPage(
|
data class CategoryDetailsPage(
|
||||||
val category: CategoryWithBalanceResponse,
|
val category: CategoryWithBalanceResponse,
|
||||||
val budget: BudgetResponse,
|
val budget: BudgetResponse,
|
||||||
val budgets: List<BudgetResponse>,
|
|
||||||
val transactionCount: String,
|
val transactionCount: String,
|
||||||
val transactions: List<Map.Entry<String, List<TransactionListItem>>>,
|
val transactions: List<Map.Entry<String, List<TransactionListItem>>>,
|
||||||
|
override val budgets: List<BudgetListItem>,
|
||||||
override val user: UserResponse,
|
override val user: UserResponse,
|
||||||
override val error: String? = null
|
override val error: String? = null
|
||||||
) : AuthenticatedPage {
|
) : AuthenticatedPage {
|
||||||
|
@ -42,6 +43,7 @@ data class CategoryFormPage(
|
||||||
val category: CategoryResponse,
|
val category: CategoryResponse,
|
||||||
val amountLabel: String,
|
val amountLabel: String,
|
||||||
val budget: BudgetResponse,
|
val budget: BudgetResponse,
|
||||||
|
override val budgets: List<BudgetListItem>,
|
||||||
override val user: UserResponse,
|
override val user: UserResponse,
|
||||||
override val error: String? = null
|
override val error: String? = null
|
||||||
) : AuthenticatedPage {
|
) : AuthenticatedPage {
|
||||||
|
|
|
@ -12,10 +12,8 @@ import com.wbrawner.twigs.service.transaction.TransactionService
|
||||||
import com.wbrawner.twigs.service.user.UserService
|
import com.wbrawner.twigs.service.user.UserService
|
||||||
import com.wbrawner.twigs.toInstant
|
import com.wbrawner.twigs.toInstant
|
||||||
import com.wbrawner.twigs.toInstantOrNull
|
import com.wbrawner.twigs.toInstantOrNull
|
||||||
import com.wbrawner.twigs.web.NotFoundPage
|
import com.wbrawner.twigs.web.*
|
||||||
import com.wbrawner.twigs.web.budget.toCurrencyString
|
import com.wbrawner.twigs.web.budget.toCurrencyString
|
||||||
import com.wbrawner.twigs.web.getAmount
|
|
||||||
import com.wbrawner.twigs.web.toDecimalString
|
|
||||||
import com.wbrawner.twigs.web.user.TWIGS_SESSION_COOKIE
|
import com.wbrawner.twigs.web.user.TWIGS_SESSION_COOKIE
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
@ -26,7 +24,6 @@ import io.ktor.server.response.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
import io.ktor.server.util.*
|
import io.ktor.server.util.*
|
||||||
import io.ktor.util.date.*
|
import io.ktor.util.date.*
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -49,6 +46,8 @@ fun Application.categoryWebRoutes(
|
||||||
get {
|
get {
|
||||||
val user = userService.user(requireSession().userId)
|
val user = userService.user(requireSession().userId)
|
||||||
val budgetId = call.parameters.getOrFail("budgetId")
|
val budgetId = call.parameters.getOrFail("budgetId")
|
||||||
|
val budgets = budgetService.budgetsForUser(user.id)
|
||||||
|
val budget = budgets.first { it.id == budgetId }
|
||||||
call.respond(
|
call.respond(
|
||||||
MustacheContent(
|
MustacheContent(
|
||||||
"category-form.mustache",
|
"category-form.mustache",
|
||||||
|
@ -63,7 +62,8 @@ fun Application.categoryWebRoutes(
|
||||||
archived = false,
|
archived = false,
|
||||||
),
|
),
|
||||||
amountLabel = 0L.toDecimalString(),
|
amountLabel = 0L.toDecimalString(),
|
||||||
budget = budgetService.budget(budgetId = budgetId, userId = user.id),
|
budget = budget,
|
||||||
|
budgets = budgets.map { it.toBudgetListItem(budgetId) },
|
||||||
user = user
|
user = user
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -73,6 +73,8 @@ fun Application.categoryWebRoutes(
|
||||||
post {
|
post {
|
||||||
val user = userService.user(requireSession().userId)
|
val user = userService.user(requireSession().userId)
|
||||||
val budgetId = call.parameters.getOrFail("budgetId")
|
val budgetId = call.parameters.getOrFail("budgetId")
|
||||||
|
val budgets = budgetService.budgetsForUser(user.id)
|
||||||
|
val budget = budgets.first { it.id == budgetId }
|
||||||
try {
|
try {
|
||||||
val request = call.receiveParameters().toCategoryRequest(budgetId)
|
val request = call.receiveParameters().toCategoryRequest(budgetId)
|
||||||
val category = categoryService.save(request, user.id)
|
val category = categoryService.save(request, user.id)
|
||||||
|
@ -93,7 +95,8 @@ fun Application.categoryWebRoutes(
|
||||||
budgetId = budgetId
|
budgetId = budgetId
|
||||||
),
|
),
|
||||||
amountLabel = call.parameters["amount"]?.toLongOrNull().toDecimalString(),
|
amountLabel = call.parameters["amount"]?.toLongOrNull().toDecimalString(),
|
||||||
budget = budgetService.budget(budgetId = budgetId, userId = user.id),
|
budget = budget,
|
||||||
|
budgets = budgets.map { it.toBudgetListItem(budgetId) },
|
||||||
user = user,
|
user = user,
|
||||||
error = e.message
|
error = e.message
|
||||||
)
|
)
|
||||||
|
@ -107,18 +110,18 @@ fun Application.categoryWebRoutes(
|
||||||
get {
|
get {
|
||||||
val user = userService.user(requireSession().userId)
|
val user = userService.user(requireSession().userId)
|
||||||
val categoryId = call.parameters.getOrFail("id")
|
val categoryId = call.parameters.getOrFail("id")
|
||||||
// TODO: Allow user-configurable locale
|
|
||||||
val numberFormat = NumberFormat.getCurrencyInstance(Locale.US)
|
|
||||||
try {
|
try {
|
||||||
val category = categoryService.category(categoryId = categoryId, userId = user.id)
|
val category = categoryService.category(categoryId = categoryId, userId = user.id)
|
||||||
val categoryBalance =
|
val categoryBalance =
|
||||||
abs(transactionService.sum(categoryId = category.id, userId = user.id))
|
abs(transactionService.sum(categoryId = category.id, userId = user.id))
|
||||||
val categoryWithBalance = CategoryWithBalanceResponse(
|
val categoryWithBalance = CategoryWithBalanceResponse(
|
||||||
category = category,
|
category = category,
|
||||||
amountLabel = category.amount.toCurrencyString(numberFormat),
|
amountLabel = category.amount.toCurrencyString(currencyFormat),
|
||||||
balance = categoryBalance,
|
balance = categoryBalance,
|
||||||
balanceLabel = categoryBalance.toCurrencyString(numberFormat),
|
balanceLabel = categoryBalance.toCurrencyString(currencyFormat),
|
||||||
remainingAmountLabel = (category.amount - categoryBalance).toCurrencyString(numberFormat)
|
remainingAmountLabel = (category.amount - categoryBalance).toCurrencyString(
|
||||||
|
currencyFormat
|
||||||
|
)
|
||||||
)
|
)
|
||||||
val transactions = transactionService.transactions(
|
val transactions = transactionService.transactions(
|
||||||
budgetIds = listOf(category.budgetId),
|
budgetIds = listOf(category.budgetId),
|
||||||
|
@ -129,13 +132,12 @@ fun Application.categoryWebRoutes(
|
||||||
)
|
)
|
||||||
val transactionCount = NumberFormat.getNumberInstance(Locale.US)
|
val transactionCount = NumberFormat.getNumberInstance(Locale.US)
|
||||||
.format(transactions.size)
|
.format(transactions.size)
|
||||||
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US)
|
|
||||||
val transactionsByDate = transactions.groupBy {
|
val transactionsByDate = transactions.groupBy {
|
||||||
dateFormat.format(it.date.toInstant().toGMTDate().toJvmDate())
|
shortDateFormat.format(it.date.toInstant().toGMTDate().toJvmDate())
|
||||||
}
|
}
|
||||||
.mapValues { (_, transactions) -> transactions.map { it.toListItem(numberFormat) } }
|
.mapValues { (_, transactions) -> transactions.map { it.toListItem(currencyFormat) } }
|
||||||
.entries
|
.entries
|
||||||
.sortedBy { it.key }
|
.sortedByDescending { it.key }
|
||||||
val budgets = budgetService.budgetsForUser(user.id)
|
val budgets = budgetService.budgetsForUser(user.id)
|
||||||
val budgetId = call.parameters.getOrFail("budgetId")
|
val budgetId = call.parameters.getOrFail("budgetId")
|
||||||
val budget = budgets.first { it.id == budgetId }
|
val budget = budgets.first { it.id == budgetId }
|
||||||
|
@ -145,7 +147,7 @@ fun Application.categoryWebRoutes(
|
||||||
category = categoryWithBalance,
|
category = categoryWithBalance,
|
||||||
transactions = transactionsByDate,
|
transactions = transactionsByDate,
|
||||||
transactionCount = transactionCount,
|
transactionCount = transactionCount,
|
||||||
budgets = budgets,
|
budgets = budgets.map { it.toBudgetListItem(budgetId) },
|
||||||
budget = budget,
|
budget = budget,
|
||||||
user = user
|
user = user
|
||||||
)
|
)
|
||||||
|
@ -167,11 +169,17 @@ fun Application.categoryWebRoutes(
|
||||||
userId = user.id
|
userId = user.id
|
||||||
)
|
)
|
||||||
val budgetId = call.parameters.getOrFail("budgetId")
|
val budgetId = call.parameters.getOrFail("budgetId")
|
||||||
val budget = budgetService.budget(budgetId = budgetId, userId = user.id)
|
val budgets = budgetService.budgetsForUser(user.id)
|
||||||
call.respond(
|
call.respond(
|
||||||
MustacheContent(
|
MustacheContent(
|
||||||
"category-form.mustache",
|
"category-form.mustache",
|
||||||
CategoryFormPage(category, category.amount.toDecimalString(), budget, user)
|
CategoryFormPage(
|
||||||
|
category = category,
|
||||||
|
amountLabel = category.amount.toDecimalString(),
|
||||||
|
budget = budgets.first { it.id == budgetId },
|
||||||
|
budgets = budgets.map { it.toBudgetListItem(budgetId) },
|
||||||
|
user = user
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -179,6 +187,7 @@ fun Application.categoryWebRoutes(
|
||||||
post {
|
post {
|
||||||
val user = userService.user(requireSession().userId)
|
val user = userService.user(requireSession().userId)
|
||||||
val budgetId = call.parameters.getOrFail("budgetId")
|
val budgetId = call.parameters.getOrFail("budgetId")
|
||||||
|
val budgets = budgetService.budgetsForUser(user.id)
|
||||||
val categoryId = call.parameters.getOrFail("id")
|
val categoryId = call.parameters.getOrFail("id")
|
||||||
try {
|
try {
|
||||||
val request = call.receiveParameters().toCategoryRequest(budgetId)
|
val request = call.receiveParameters().toCategoryRequest(budgetId)
|
||||||
|
@ -200,7 +209,8 @@ fun Application.categoryWebRoutes(
|
||||||
budgetId = budgetId
|
budgetId = budgetId
|
||||||
),
|
),
|
||||||
amountLabel = call.parameters["amount"]?.toLongOrNull().toDecimalString(),
|
amountLabel = call.parameters["amount"]?.toLongOrNull().toDecimalString(),
|
||||||
budget = budgetService.budget(budgetId = budgetId, userId = user.id),
|
budget = budgets.first { it.id == budgetId },
|
||||||
|
budgets = budgets.map { it.toBudgetListItem(budgetId) },
|
||||||
user = user,
|
user = user,
|
||||||
error = e.message
|
error = e.message
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,19 @@ import com.wbrawner.twigs.service.category.CategoryResponse
|
||||||
import com.wbrawner.twigs.service.transaction.TransactionResponse
|
import com.wbrawner.twigs.service.transaction.TransactionResponse
|
||||||
import com.wbrawner.twigs.service.user.UserResponse
|
import com.wbrawner.twigs.service.user.UserResponse
|
||||||
import com.wbrawner.twigs.web.AuthenticatedPage
|
import com.wbrawner.twigs.web.AuthenticatedPage
|
||||||
|
import com.wbrawner.twigs.web.BudgetListItem
|
||||||
|
import com.wbrawner.twigs.web.category.TransactionListItem
|
||||||
|
|
||||||
|
data class TransactionListPage(
|
||||||
|
val budget: BudgetResponse,
|
||||||
|
val transactions: List<Map.Entry<String, List<TransactionListItem>>>,
|
||||||
|
override val budgets: List<BudgetListItem>,
|
||||||
|
override val user: UserResponse,
|
||||||
|
override val error: String? = null
|
||||||
|
) : AuthenticatedPage {
|
||||||
|
override val title: String = "Transactions"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
data class TransactionDetailsPage(
|
data class TransactionDetailsPage(
|
||||||
val transaction: TransactionResponse,
|
val transaction: TransactionResponse,
|
||||||
|
@ -12,8 +25,8 @@ data class TransactionDetailsPage(
|
||||||
val budget: BudgetResponse,
|
val budget: BudgetResponse,
|
||||||
val amountLabel: String,
|
val amountLabel: String,
|
||||||
val dateLabel: String,
|
val dateLabel: String,
|
||||||
val budgets: List<BudgetResponse>,
|
|
||||||
val createdBy: UserResponse,
|
val createdBy: UserResponse,
|
||||||
|
override val budgets: List<BudgetListItem>,
|
||||||
override val user: UserResponse,
|
override val user: UserResponse,
|
||||||
override val error: String? = null
|
override val error: String? = null
|
||||||
) : AuthenticatedPage {
|
) : AuthenticatedPage {
|
||||||
|
@ -25,6 +38,7 @@ data class TransactionFormPage(
|
||||||
val amountLabel: String,
|
val amountLabel: String,
|
||||||
val budget: BudgetResponse,
|
val budget: BudgetResponse,
|
||||||
val categoryOptions: List<CategoryOption>,
|
val categoryOptions: List<CategoryOption>,
|
||||||
|
override val budgets: List<BudgetListItem>,
|
||||||
override val user: UserResponse,
|
override val user: UserResponse,
|
||||||
override val error: String? = null
|
override val error: String? = null
|
||||||
) : AuthenticatedPage {
|
) : AuthenticatedPage {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.wbrawner.twigs.web.transaction
|
package com.wbrawner.twigs.web.transaction
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.endOfMonth
|
||||||
|
import com.wbrawner.twigs.firstOfMonth
|
||||||
import com.wbrawner.twigs.service.HttpException
|
import com.wbrawner.twigs.service.HttpException
|
||||||
import com.wbrawner.twigs.service.budget.BudgetService
|
import com.wbrawner.twigs.service.budget.BudgetService
|
||||||
import com.wbrawner.twigs.service.category.CategoryService
|
import com.wbrawner.twigs.service.category.CategoryService
|
||||||
|
@ -10,11 +12,10 @@ import com.wbrawner.twigs.service.transaction.TransactionService
|
||||||
import com.wbrawner.twigs.service.user.UserResponse
|
import com.wbrawner.twigs.service.user.UserResponse
|
||||||
import com.wbrawner.twigs.service.user.UserService
|
import com.wbrawner.twigs.service.user.UserService
|
||||||
import com.wbrawner.twigs.toInstant
|
import com.wbrawner.twigs.toInstant
|
||||||
import com.wbrawner.twigs.web.NotFoundPage
|
import com.wbrawner.twigs.toInstantOrNull
|
||||||
|
import com.wbrawner.twigs.web.*
|
||||||
import com.wbrawner.twigs.web.budget.toCurrencyString
|
import com.wbrawner.twigs.web.budget.toCurrencyString
|
||||||
import com.wbrawner.twigs.web.currencyFormat
|
import com.wbrawner.twigs.web.category.toListItem
|
||||||
import com.wbrawner.twigs.web.getAmount
|
|
||||||
import com.wbrawner.twigs.web.toDecimalString
|
|
||||||
import com.wbrawner.twigs.web.user.TWIGS_SESSION_COOKIE
|
import com.wbrawner.twigs.web.user.TWIGS_SESSION_COOKIE
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
@ -24,6 +25,7 @@ import io.ktor.server.request.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
import io.ktor.server.util.*
|
import io.ktor.server.util.*
|
||||||
|
import io.ktor.util.date.*
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneOffset.UTC
|
import java.time.ZoneOffset.UTC
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
@ -39,15 +41,40 @@ fun Application.transactionWebRoutes(
|
||||||
authenticate(TWIGS_SESSION_COOKIE) {
|
authenticate(TWIGS_SESSION_COOKIE) {
|
||||||
route("/budgets/{budgetId}/transactions") {
|
route("/budgets/{budgetId}/transactions") {
|
||||||
get {
|
get {
|
||||||
// TODO: Show transaction list here
|
val user = userService.user(requireSession().userId)
|
||||||
val budgetId = call.parameters.getOrFail("budgetId")
|
val budgetId = call.parameters.getOrFail("budgetId")
|
||||||
call.respondRedirect("/budgets/$budgetId")
|
val budgets = budgetService.budgetsForUser(user.id)
|
||||||
|
val transactions = transactionService.transactions(
|
||||||
|
budgetIds = listOf(budgetId),
|
||||||
|
from = call.parameters["from"]?.toInstantOrNull() ?: firstOfMonth,
|
||||||
|
to = call.parameters["to"]?.toInstantOrNull() ?: endOfMonth,
|
||||||
|
userId = user.id
|
||||||
|
)
|
||||||
|
val transactionsByDate = transactions.groupBy {
|
||||||
|
shortDateFormat.format(it.date.toInstant().toGMTDate().toJvmDate())
|
||||||
|
}
|
||||||
|
.mapValues { (_, transactions) -> transactions.map { it.toListItem(currencyFormat) } }
|
||||||
|
.entries
|
||||||
|
.sortedByDescending { it.key }
|
||||||
|
call.respond(
|
||||||
|
MustacheContent(
|
||||||
|
"budget-transactions.mustache",
|
||||||
|
TransactionListPage(
|
||||||
|
budgets = budgets.map { it.toBudgetListItem(budgetId) },
|
||||||
|
budget = budgets.first { it.id == budgetId },
|
||||||
|
transactions = transactionsByDate,
|
||||||
|
user = user
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
route("/new") {
|
route("/new") {
|
||||||
get {
|
get {
|
||||||
val user = userService.user(requireSession().userId)
|
val user = userService.user(requireSession().userId)
|
||||||
val budgetId = call.parameters.getOrFail("budgetId")
|
val budgetId = call.parameters.getOrFail("budgetId")
|
||||||
|
val budgets = budgetService.budgetsForUser(user.id)
|
||||||
|
val budget = budgets.first { it.id == budgetId }
|
||||||
val categoryId = call.request.queryParameters["categoryId"]
|
val categoryId = call.request.queryParameters["categoryId"]
|
||||||
val transaction = TransactionResponse(
|
val transaction = TransactionResponse(
|
||||||
id = "",
|
id = "",
|
||||||
|
@ -66,13 +93,14 @@ fun Application.transactionWebRoutes(
|
||||||
TransactionFormPage(
|
TransactionFormPage(
|
||||||
transaction = transaction,
|
transaction = transaction,
|
||||||
amountLabel = 0L.toDecimalString(),
|
amountLabel = 0L.toDecimalString(),
|
||||||
budget = budgetService.budget(budgetId = budgetId, userId = user.id),
|
budget = budget,
|
||||||
categoryOptions = categoryOptions(
|
categoryOptions = categoryOptions(
|
||||||
transaction = transaction,
|
transaction = transaction,
|
||||||
categoryService = categoryService,
|
categoryService = categoryService,
|
||||||
budgetId = budgetId,
|
budgetId = budgetId,
|
||||||
user = user
|
user = user
|
||||||
),
|
),
|
||||||
|
budgets = budgets.map { it.toBudgetListItem(budgetId) },
|
||||||
user = user
|
user = user
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -82,6 +110,8 @@ fun Application.transactionWebRoutes(
|
||||||
post {
|
post {
|
||||||
val user = userService.user(requireSession().userId)
|
val user = userService.user(requireSession().userId)
|
||||||
val urlBudgetId = call.parameters.getOrFail("budgetId")
|
val urlBudgetId = call.parameters.getOrFail("budgetId")
|
||||||
|
val budgets = budgetService.budgetsForUser(user.id)
|
||||||
|
val budget = budgets.first { it.id == urlBudgetId }
|
||||||
try {
|
try {
|
||||||
val request = call.receiveParameters().toTransactionRequest()
|
val request = call.receiveParameters().toTransactionRequest()
|
||||||
.run {
|
.run {
|
||||||
|
@ -115,13 +145,14 @@ fun Application.transactionWebRoutes(
|
||||||
TransactionFormPage(
|
TransactionFormPage(
|
||||||
transaction = transaction,
|
transaction = transaction,
|
||||||
amountLabel = call.parameters["amount"].orEmpty(),
|
amountLabel = call.parameters["amount"].orEmpty(),
|
||||||
budget = budgetService.budget(budgetId = urlBudgetId, userId = user.id),
|
budget = budget,
|
||||||
categoryOptions = categoryOptions(
|
categoryOptions = categoryOptions(
|
||||||
transaction,
|
transaction,
|
||||||
categoryService,
|
categoryService,
|
||||||
urlBudgetId,
|
urlBudgetId,
|
||||||
user
|
user
|
||||||
),
|
),
|
||||||
|
budgets = budgets.map { it.toBudgetListItem(urlBudgetId) },
|
||||||
user = user,
|
user = user,
|
||||||
error = e.message
|
error = e.message
|
||||||
)
|
)
|
||||||
|
@ -161,7 +192,7 @@ fun Application.transactionWebRoutes(
|
||||||
transaction = transaction,
|
transaction = transaction,
|
||||||
category = category,
|
category = category,
|
||||||
budget = budget,
|
budget = budget,
|
||||||
budgets = budgets,
|
budgets = budgets.map { it.toBudgetListItem(budgetId) },
|
||||||
amountLabel = transaction.amount?.toCurrencyString(currencyFormat).orEmpty(),
|
amountLabel = transaction.amount?.toCurrencyString(currencyFormat).orEmpty(),
|
||||||
dateLabel = dateLabel,
|
dateLabel = dateLabel,
|
||||||
createdBy = userService.user(transaction.createdBy),
|
createdBy = userService.user(transaction.createdBy),
|
||||||
|
@ -181,6 +212,8 @@ fun Application.transactionWebRoutes(
|
||||||
get {
|
get {
|
||||||
val user = userService.user(requireSession().userId)
|
val user = userService.user(requireSession().userId)
|
||||||
val budgetId = call.parameters.getOrFail("budgetId")
|
val budgetId = call.parameters.getOrFail("budgetId")
|
||||||
|
val budgets = budgetService.budgetsForUser(user.id)
|
||||||
|
val budget = budgets.first { it.id == budgetId }
|
||||||
val transaction = transactionService.transaction(
|
val transaction = transactionService.transaction(
|
||||||
transactionId = call.parameters.getOrFail("id"),
|
transactionId = call.parameters.getOrFail("id"),
|
||||||
userId = user.id
|
userId = user.id
|
||||||
|
@ -193,8 +226,9 @@ fun Application.transactionWebRoutes(
|
||||||
date = transaction.date.toInstant().toHtmlInputString()
|
date = transaction.date.toInstant().toHtmlInputString()
|
||||||
),
|
),
|
||||||
amountLabel = transaction.amount.toDecimalString(),
|
amountLabel = transaction.amount.toDecimalString(),
|
||||||
budget = budgetService.budget(budgetId = budgetId, userId = user.id),
|
budget = budget,
|
||||||
categoryOptions = categoryOptions(transaction, categoryService, budgetId, user),
|
categoryOptions = categoryOptions(transaction, categoryService, budgetId, user),
|
||||||
|
budgets = budgets.map { it.toBudgetListItem(budgetId) },
|
||||||
user = user
|
user = user
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -205,6 +239,8 @@ fun Application.transactionWebRoutes(
|
||||||
val user = userService.user(requireSession().userId)
|
val user = userService.user(requireSession().userId)
|
||||||
val transactionId = call.parameters.getOrFail("id")
|
val transactionId = call.parameters.getOrFail("id")
|
||||||
val urlBudgetId = call.parameters.getOrFail("budgetId")
|
val urlBudgetId = call.parameters.getOrFail("budgetId")
|
||||||
|
val budgets = budgetService.budgetsForUser(user.id)
|
||||||
|
val budget = budgets.first { it.id == urlBudgetId }
|
||||||
try {
|
try {
|
||||||
val request = call.receiveParameters().toTransactionRequest()
|
val request = call.receiveParameters().toTransactionRequest()
|
||||||
.run {
|
.run {
|
||||||
|
@ -239,13 +275,14 @@ fun Application.transactionWebRoutes(
|
||||||
TransactionFormPage(
|
TransactionFormPage(
|
||||||
transaction = transaction,
|
transaction = transaction,
|
||||||
amountLabel = call.parameters["amount"].orEmpty(),
|
amountLabel = call.parameters["amount"].orEmpty(),
|
||||||
budget = budgetService.budget(budgetId = urlBudgetId, userId = user.id),
|
budget = budget,
|
||||||
categoryOptions = categoryOptions(
|
categoryOptions = categoryOptions(
|
||||||
transaction,
|
transaction,
|
||||||
categoryService,
|
categoryService,
|
||||||
urlBudgetId,
|
urlBudgetId,
|
||||||
user
|
user
|
||||||
),
|
),
|
||||||
|
budgets = budgets.map { it.toBudgetListItem(urlBudgetId) },
|
||||||
user = user,
|
user = user,
|
||||||
error = e.message
|
error = e.message
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
{{> partials/head }}
|
||||||
|
<div id="app">
|
||||||
|
{{>partials/sidebar}}
|
||||||
|
<main>
|
||||||
|
<div class="column">
|
||||||
|
<header class="row">
|
||||||
|
<a id="hamburger" href="#sidebar">☰</a>
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<a href="/budgets/{{budget.id}}/transactions/new"
|
||||||
|
class="button button-secondary">
|
||||||
|
<!-- TODO: Hide text on small widths -->
|
||||||
|
<span aria-description="New Transaction">+</span> <span
|
||||||
|
aria-hidden="true">New Transaction</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<!-- TODO: Add a search bar to filter transactions by name/description -->
|
||||||
|
<ul>
|
||||||
|
{{#transactions}}
|
||||||
|
{{>partials/transaction-list}}
|
||||||
|
{{/transactions}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{{>partials/foot}}
|
Loading…
Reference in a new issue