Add transaction form page
This commit is contained in:
parent
5a9b988c63
commit
64e7cb9d52
3 changed files with 88 additions and 17 deletions
|
@ -22,6 +22,7 @@ data class TransactionDetailsPage(
|
||||||
|
|
||||||
data class TransactionFormPage(
|
data class TransactionFormPage(
|
||||||
val transaction: TransactionResponse,
|
val transaction: TransactionResponse,
|
||||||
|
val amountLabel: String,
|
||||||
val budget: BudgetResponse,
|
val budget: BudgetResponse,
|
||||||
val incomeCategories: List<CategoryResponse>,
|
val incomeCategories: List<CategoryResponse>,
|
||||||
val expenseCategories: List<CategoryResponse>,
|
val expenseCategories: List<CategoryResponse>,
|
||||||
|
@ -29,8 +30,8 @@ data class TransactionFormPage(
|
||||||
override val error: String? = null
|
override val error: String? = null
|
||||||
) : AuthenticatedPage {
|
) : AuthenticatedPage {
|
||||||
override val title: String = if (transaction.id.isBlank()) {
|
override val title: String = if (transaction.id.isBlank()) {
|
||||||
"New Category"
|
"New Transaction"
|
||||||
} else {
|
} else {
|
||||||
"Edit Category"
|
"Edit Transaction"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,15 +20,25 @@ 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 java.math.BigDecimal
|
||||||
|
import java.text.DecimalFormat
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
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
|
||||||
import java.time.format.FormatStyle
|
import java.time.format.FormatStyle
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
private val dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
|
private val dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
|
||||||
private val numberFormat = NumberFormat.getCurrencyInstance(Locale.US)
|
private val currencyFormat = NumberFormat.getCurrencyInstance(Locale.US)
|
||||||
|
private val decimalFormat = DecimalFormat.getNumberInstance(Locale.US).apply {
|
||||||
|
with(this as DecimalFormat) {
|
||||||
|
decimalFormatSymbols = decimalFormatSymbols.apply {
|
||||||
|
currencySymbol = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Application.transactionWebRoutes(
|
fun Application.transactionWebRoutes(
|
||||||
budgetService: BudgetService,
|
budgetService: BudgetService,
|
||||||
|
@ -60,10 +70,11 @@ fun Application.transactionWebRoutes(
|
||||||
amount = 0,
|
amount = 0,
|
||||||
budgetId = budgetId,
|
budgetId = budgetId,
|
||||||
expense = true,
|
expense = true,
|
||||||
date = dateTimeFormatter.format(Instant.now()),
|
date = Instant.now().toHtmlInputString(),
|
||||||
categoryId = null,
|
categoryId = null,
|
||||||
createdBy = user.id
|
createdBy = user.id
|
||||||
),
|
),
|
||||||
|
amountLabel = currencyFormat.format(0L),
|
||||||
budget = budgetService.budget(budgetId = budgetId, userId = user.id),
|
budget = budgetService.budget(budgetId = budgetId, userId = user.id),
|
||||||
incomeCategories = categoryService.categories(
|
incomeCategories = categoryService.categories(
|
||||||
budgetIds = listOf(budgetId),
|
budgetIds = listOf(budgetId),
|
||||||
|
@ -77,7 +88,7 @@ fun Application.transactionWebRoutes(
|
||||||
expense = true,
|
expense = true,
|
||||||
archived = false
|
archived = false
|
||||||
),
|
),
|
||||||
user
|
user = user
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -85,11 +96,21 @@ fun Application.transactionWebRoutes(
|
||||||
|
|
||||||
post {
|
post {
|
||||||
val user = userService.user(requireSession().userId)
|
val user = userService.user(requireSession().userId)
|
||||||
val budgetId = call.parameters.getOrFail("budgetId")
|
val urlBudgetId = call.parameters.getOrFail("budgetId")
|
||||||
try {
|
try {
|
||||||
val request = call.receiveParameters().toTransactionRequest()
|
val request = call.receiveParameters().toTransactionRequest()
|
||||||
|
.run {
|
||||||
|
copy(
|
||||||
|
date = date + 'Z',
|
||||||
|
expense = categoryService.category(
|
||||||
|
categoryId = requireNotNull(categoryId),
|
||||||
|
userId = user.id
|
||||||
|
).expense,
|
||||||
|
budgetId = urlBudgetId
|
||||||
|
)
|
||||||
|
}
|
||||||
val transaction = transactionService.save(request, user.id)
|
val transaction = transactionService.save(request, user.id)
|
||||||
call.respondRedirect("/budgets/${transaction.budgetId}/categories/${transaction.id}")
|
call.respondRedirect("/budgets/${transaction.budgetId}/transactions/${transaction.id}")
|
||||||
} catch (e: HttpException) {
|
} catch (e: HttpException) {
|
||||||
call.respond(
|
call.respond(
|
||||||
status = e.statusCode,
|
status = e.statusCode,
|
||||||
|
@ -100,27 +121,29 @@ fun Application.transactionWebRoutes(
|
||||||
id = "",
|
id = "",
|
||||||
title = call.parameters["title"],
|
title = call.parameters["title"],
|
||||||
description = call.parameters["description"],
|
description = call.parameters["description"],
|
||||||
amount = call.parameters["amount"]?.toLongOrNull(),
|
amount = 0L,
|
||||||
budgetId = budgetId,
|
budgetId = urlBudgetId,
|
||||||
expense = call.parameters["expense"]?.toBoolean() ?: true,
|
expense = call.parameters["expense"]?.toBoolean() ?: true,
|
||||||
date = call.parameters["date"].orEmpty(),
|
date = call.parameters["date"].orEmpty(),
|
||||||
categoryId = call.parameters["categoryId"],
|
categoryId = call.parameters["categoryId"],
|
||||||
createdBy = user.id
|
createdBy = user.id
|
||||||
),
|
),
|
||||||
budget = budgetService.budget(budgetId = budgetId, userId = user.id),
|
amountLabel = call.parameters["amount"].orEmpty(),
|
||||||
|
budget = budgetService.budget(budgetId = urlBudgetId, userId = user.id),
|
||||||
incomeCategories = categoryService.categories(
|
incomeCategories = categoryService.categories(
|
||||||
budgetIds = listOf(budgetId),
|
budgetIds = listOf(urlBudgetId),
|
||||||
userId = user.id,
|
userId = user.id,
|
||||||
expense = false,
|
expense = false,
|
||||||
archived = false
|
archived = false
|
||||||
),
|
),
|
||||||
expenseCategories = categoryService.categories(
|
expenseCategories = categoryService.categories(
|
||||||
budgetIds = listOf(budgetId),
|
budgetIds = listOf(urlBudgetId),
|
||||||
userId = user.id,
|
userId = user.id,
|
||||||
expense = true,
|
expense = true,
|
||||||
archived = false
|
archived = false
|
||||||
),
|
),
|
||||||
user
|
user = user,
|
||||||
|
error = e.message
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -159,7 +182,7 @@ fun Application.transactionWebRoutes(
|
||||||
category = category,
|
category = category,
|
||||||
budget = budget,
|
budget = budget,
|
||||||
budgets = budgets,
|
budgets = budgets,
|
||||||
amountLabel = transaction.amount?.toCurrencyString(numberFormat).orEmpty(),
|
amountLabel = transaction.amount?.toCurrencyString(currencyFormat).orEmpty(),
|
||||||
dateLabel = dateLabel,
|
dateLabel = dateLabel,
|
||||||
createdBy = userService.user(transaction.createdBy),
|
createdBy = userService.user(transaction.createdBy),
|
||||||
user = user
|
user = user
|
||||||
|
@ -173,6 +196,10 @@ fun Application.transactionWebRoutes(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,9 +209,11 @@ fun Application.transactionWebRoutes(
|
||||||
private fun Parameters.toTransactionRequest() = TransactionRequest(
|
private fun Parameters.toTransactionRequest() = TransactionRequest(
|
||||||
title = get("title"),
|
title = get("title"),
|
||||||
description = get("description"),
|
description = get("description"),
|
||||||
amount = get("amount")?.toLongOrNull(),
|
amount = decimalFormat.parse(get("amount"))?.toDouble()?.toBigDecimal()?.times(BigDecimal(100))?.toLong() ?: 0L,
|
||||||
expense = get("expense")?.toBoolean(),
|
expense = false,
|
||||||
date = get("date"),
|
date = get("date"),
|
||||||
categoryId = get("categoryId"),
|
categoryId = get("categoryId"),
|
||||||
budgetId = get("budgetId"),
|
budgetId = get("budgetId"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun Instant.toHtmlInputString() = truncatedTo(ChronoUnit.SECONDS).toString().substringBefore('Z')
|
41
web/src/main/resources/templates/transaction-form.mustache
Normal file
41
web/src/main/resources/templates/transaction-form.mustache
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{{> partials/head }}
|
||||||
|
<div id="app">
|
||||||
|
<main>
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
<div class="center">
|
||||||
|
{{#error }}
|
||||||
|
<p class="error">{{error}}</p>
|
||||||
|
{{/error}}
|
||||||
|
<form method="post">
|
||||||
|
<label for="title">Name</label>
|
||||||
|
<input id="title" type="text" name="title" value="{{ transaction.title }}" required/>
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea id="description" name="description">{{ transaction.description }}</textarea>
|
||||||
|
<label for="date">Date</label>
|
||||||
|
<input id="date" type="datetime-local" name="date" value="{{ transaction.date }}" required/>
|
||||||
|
<label for="amount">Amount</label>
|
||||||
|
<input id="amount" type="number" name="amount" value="{{ amountLabel }}" required/>
|
||||||
|
<label for="categoryId">Category</label>
|
||||||
|
<select id="categoryId" name="categoryId" required>
|
||||||
|
<option selected disabled>Select a category</option>
|
||||||
|
<option disabled>Income</option>
|
||||||
|
{{#incomeCategories}}
|
||||||
|
<option value="{{id}}">{{title}}</option>
|
||||||
|
{{/incomeCategories}}
|
||||||
|
<option disabled>Expense</option>
|
||||||
|
{{#expenseCategories}}
|
||||||
|
<option value="{{id}}">{{title}}</option>
|
||||||
|
{{/expenseCategories}}
|
||||||
|
</select>
|
||||||
|
<input id="submit" type="submit" class="button button-primary" value="Save"/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
const dateField = document.querySelector('#date')
|
||||||
|
let localDateTime = dateField.valueAsDate
|
||||||
|
const localTimeMs = localDateTime.setMinutes(localDateTime.getMinutes() - localDateTime.getTimezoneOffset())
|
||||||
|
dateField.value = new Date(localTimeMs).toISOString().slice(0, -1)
|
||||||
|
</script>
|
||||||
|
{{>partials/foot}}
|
Loading…
Reference in a new issue