Implement transaction list page and ensure consistency with budget list in sidebar

This commit is contained in:
William Brawner 2024-04-20 21:38:39 -06:00
parent 689dbc39e2
commit d8b89bc8d2
9 changed files with 158 additions and 51 deletions

View file

@ -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

View file

@ -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()

View file

@ -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 {

View file

@ -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"),

View file

@ -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 {

View file

@ -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
) )

View file

@ -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 {

View file

@ -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
) )

View file

@ -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}}