diff --git a/web/src/main/kotlin/com/wbrawner/twigs/web/WebUtils.kt b/web/src/main/kotlin/com/wbrawner/twigs/web/WebUtils.kt new file mode 100644 index 0000000..06c1d2e --- /dev/null +++ b/web/src/main/kotlin/com/wbrawner/twigs/web/WebUtils.kt @@ -0,0 +1,30 @@ +package com.wbrawner.twigs.web + +import io.ktor.http.* +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.DecimalFormat +import java.text.NumberFormat +import java.util.* + +val currencyFormat = NumberFormat.getCurrencyInstance(Locale.US) +val decimalFormat = DecimalFormat.getNumberInstance(Locale.US).apply { + with(this as DecimalFormat) { + decimalFormatSymbols = decimalFormatSymbols.apply { + currencySymbol = "" + isGroupingUsed = false + } + } +} + +fun Parameters.getAmount() = decimalFormat.parse(get("amount")) + ?.toDouble() + ?.toBigDecimal() + ?.times(BigDecimal(100)) + ?.toLong() + ?: 0L + +fun Long?.toDecimalString(): String { + if (this == null) return "" + return decimalFormat.format(toBigDecimal().divide(BigDecimal(100), 2, RoundingMode.HALF_UP)) +} \ No newline at end of file diff --git a/web/src/main/kotlin/com/wbrawner/twigs/web/category/CategoryPages.kt b/web/src/main/kotlin/com/wbrawner/twigs/web/category/CategoryPages.kt index 81bb9d2..fb99183 100644 --- a/web/src/main/kotlin/com/wbrawner/twigs/web/category/CategoryPages.kt +++ b/web/src/main/kotlin/com/wbrawner/twigs/web/category/CategoryPages.kt @@ -40,6 +40,7 @@ fun TransactionResponse.toListItem(numberFormat: NumberFormat) = TransactionList data class CategoryFormPage( val category: CategoryResponse, + val amountLabel: String, val budget: BudgetResponse, override val user: UserResponse, override val error: String? = null @@ -49,6 +50,10 @@ data class CategoryFormPage( } else { "Edit Category" } + + val expenseChecked: String = if (category.expense) "checked" else "" + val incomeChecked: String = if (!category.expense) "checked" else "" + val archivedChecked: String = if (category.archived) "checked" else "" } data class CategoryWithBalanceResponse( diff --git a/web/src/main/kotlin/com/wbrawner/twigs/web/category/CategoryWebRoutes.kt b/web/src/main/kotlin/com/wbrawner/twigs/web/category/CategoryWebRoutes.kt index 9f0b3f1..193503d 100644 --- a/web/src/main/kotlin/com/wbrawner/twigs/web/category/CategoryWebRoutes.kt +++ b/web/src/main/kotlin/com/wbrawner/twigs/web/category/CategoryWebRoutes.kt @@ -14,6 +14,8 @@ import com.wbrawner.twigs.toInstant import com.wbrawner.twigs.toInstantOrNull import com.wbrawner.twigs.web.NotFoundPage 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 io.ktor.http.* import io.ktor.server.application.* @@ -57,9 +59,10 @@ fun Application.categoryWebRoutes( description = "", amount = 0, budgetId = budgetId, - expense = true, + expense = call.request.queryParameters["expense"]?.toBoolean() ?: true, archived = false, ), + amountLabel = 0L.toDecimalString(), budget = budgetService.budget(budgetId = budgetId, userId = user.id), user = user ) @@ -71,7 +74,7 @@ fun Application.categoryWebRoutes( val user = userService.user(requireSession().userId) val budgetId = call.parameters.getOrFail("budgetId") try { - val request = call.receiveParameters().toCategoryRequest() + val request = call.receiveParameters().toCategoryRequest(budgetId) val category = categoryService.save(request, user.id) call.respondRedirect("/budgets/${category.budgetId}/categories/${category.id}") } catch (e: HttpException) { @@ -84,11 +87,12 @@ fun Application.categoryWebRoutes( id = "", title = call.parameters["title"].orEmpty(), description = call.parameters["description"].orEmpty(), - amount = call.parameters["amount"]?.toLongOrNull() ?: 0L, + amount = 0L, expense = call.parameters["expense"]?.toBoolean() ?: false, archived = call.parameters["archived"]?.toBoolean() ?: false, budgetId = budgetId ), + amountLabel = call.parameters["amount"]?.toLongOrNull().toDecimalString(), budget = budgetService.budget(budgetId = budgetId, userId = user.id), user = user, error = e.message @@ -167,10 +171,43 @@ fun Application.categoryWebRoutes( call.respond( MustacheContent( "category-form.mustache", - CategoryFormPage(category, budget, user) + CategoryFormPage(category, category.amount.toDecimalString(), budget, user) ) ) } + + post { + val user = userService.user(requireSession().userId) + val budgetId = call.parameters.getOrFail("budgetId") + val categoryId = call.parameters.getOrFail("id") + try { + val request = call.receiveParameters().toCategoryRequest(budgetId) + val category = categoryService.save(request, userId = user.id, categoryId = categoryId) + call.respondRedirect("/budgets/${category.budgetId}/categories/${category.id}") + } catch (e: HttpException) { + call.respond( + status = e.statusCode, + MustacheContent( + "category-form.mustache", + CategoryFormPage( + CategoryResponse( + id = "", + title = call.parameters["title"].orEmpty(), + description = call.parameters["description"].orEmpty(), + amount = 0L, + expense = call.parameters["expense"]?.toBoolean() ?: false, + archived = call.parameters["archived"]?.toBoolean() ?: false, + budgetId = budgetId + ), + amountLabel = call.parameters["amount"]?.toLongOrNull().toDecimalString(), + budget = budgetService.budget(budgetId = budgetId, userId = user.id), + user = user, + error = e.message + ) + ) + ) + } + } } } } @@ -178,11 +215,11 @@ fun Application.categoryWebRoutes( } } -private fun Parameters.toCategoryRequest() = CategoryRequest( +private fun Parameters.toCategoryRequest(budgetId: String) = CategoryRequest( title = get("title"), description = get("description"), - amount = get("amount")?.toLongOrNull(), + amount = getAmount(), expense = get("expense")?.toBoolean(), - archived = get("archived")?.toBoolean(), - budgetId = get("budgetId"), + archived = get("archived") == "on", + budgetId = budgetId ) \ No newline at end of file diff --git a/web/src/main/kotlin/com/wbrawner/twigs/web/transaction/TransactionWebRoutes.kt b/web/src/main/kotlin/com/wbrawner/twigs/web/transaction/TransactionWebRoutes.kt index 7c9ea2d..ccc2b12 100644 --- a/web/src/main/kotlin/com/wbrawner/twigs/web/transaction/TransactionWebRoutes.kt +++ b/web/src/main/kotlin/com/wbrawner/twigs/web/transaction/TransactionWebRoutes.kt @@ -12,6 +12,9 @@ import com.wbrawner.twigs.service.user.UserService import com.wbrawner.twigs.toInstant import com.wbrawner.twigs.web.NotFoundPage import com.wbrawner.twigs.web.budget.toCurrencyString +import com.wbrawner.twigs.web.currencyFormat +import com.wbrawner.twigs.web.getAmount +import com.wbrawner.twigs.web.toDecimalString import com.wbrawner.twigs.web.user.TWIGS_SESSION_COOKIE import io.ktor.http.* import io.ktor.server.application.* @@ -21,27 +24,10 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.util.* -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.DecimalFormat -import java.text.NumberFormat import java.time.Instant import java.time.ZoneOffset.UTC import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle import java.time.temporal.ChronoUnit -import java.util.* - -private val dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) -private val currencyFormat = NumberFormat.getCurrencyInstance(Locale.US) -private val decimalFormat = DecimalFormat.getNumberInstance(Locale.US).apply { - with(this as DecimalFormat) { - decimalFormatSymbols = decimalFormatSymbols.apply { - currencySymbol = "" - isGroupingUsed = false - } - } -} fun Application.transactionWebRoutes( budgetService: BudgetService, @@ -62,6 +48,7 @@ fun Application.transactionWebRoutes( get { val user = userService.user(requireSession().userId) val budgetId = call.parameters.getOrFail("budgetId") + val categoryId = call.request.queryParameters["categoryId"] val transaction = TransactionResponse( id = "", title = "", @@ -70,7 +57,7 @@ fun Application.transactionWebRoutes( budgetId = budgetId, expense = true, date = Instant.now().toHtmlInputString(), - categoryId = null, + categoryId = categoryId, createdBy = user.id ) call.respond( @@ -78,9 +65,14 @@ fun Application.transactionWebRoutes( "transaction-form.mustache", TransactionFormPage( transaction = transaction, - amountLabel = currencyFormat.format(0L), + amountLabel = 0L.toDecimalString(), budget = budgetService.budget(budgetId = budgetId, userId = user.id), - categoryOptions = categoryOptions(transaction, categoryService, budgetId, user), + categoryOptions = categoryOptions( + transaction = transaction, + categoryService = categoryService, + budgetId = budgetId, + user = user + ), user = user ) ) @@ -313,7 +305,7 @@ private suspend fun categoryOptions( private fun Parameters.toTransactionRequest() = TransactionRequest( title = get("title"), description = get("description"), - amount = decimalFormat.parse(get("amount"))?.toDouble()?.toBigDecimal()?.times(BigDecimal(100))?.toLong() ?: 0L, + amount = getAmount(), expense = false, date = get("date"), categoryId = get("categoryId"), @@ -322,7 +314,3 @@ private fun Parameters.toTransactionRequest() = TransactionRequest( private fun Instant.toHtmlInputString() = truncatedTo(ChronoUnit.MINUTES).toString().substringBefore(":00Z") -private fun Long?.toDecimalString(): String { - if (this == null) return "" - return decimalFormat.format(toBigDecimal().divide(BigDecimal(100), 2, RoundingMode.HALF_UP)) -} \ No newline at end of file diff --git a/web/src/main/resources/static/style.css b/web/src/main/resources/static/style.css index f6dd800..be659f7 100644 --- a/web/src/main/resources/static/style.css +++ b/web/src/main/resources/static/style.css @@ -44,7 +44,7 @@ h1, h2, h3, h4, h5, h6, p, ul { padding: 0; } -h1, h2, h3, h4, h5, h6, p, summary { +h2, h3, h4, h5, h6, p, summary { padding: 0.5rem; } @@ -186,6 +186,12 @@ input:disabled { color: var(--color-on-background); } +.inline-input { + display: flex; + flex-direction: row; + justify-content: start; +} + a { color: var(--color-accent); } diff --git a/web/src/main/resources/templates/category-details.mustache b/web/src/main/resources/templates/category-details.mustache index a382870..02f6f7c 100644 --- a/web/src/main/resources/templates/category-details.mustache +++ b/web/src/main/resources/templates/category-details.mustache @@ -6,11 +6,18 @@

{{title}}

- - - + - +
+ + + + + + + + + +

{{category.category.description}}

diff --git a/web/src/main/resources/templates/category-form.mustache b/web/src/main/resources/templates/category-form.mustache new file mode 100644 index 0000000..b3d534b --- /dev/null +++ b/web/src/main/resources/templates/category-form.mustache @@ -0,0 +1,31 @@ +{{> partials/head }} +
+
+

{{title}}

+ {{#error }} +

{{error}}

+ {{/error}} +
+ + + + + + +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+{{>partials/foot}}