budgit/internal/ui/forms/create_transfer.templ
2026-05-04 04:24:08 +00:00

272 lines
8.8 KiB
Text

package forms
import "git.juancwu.dev/juancwu/budgit/internal/model"
import "git.juancwu.dev/juancwu/budgit/internal/routeurl"
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/card"
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/form"
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/textarea"
import "git.juancwu.dev/juancwu/budgit/internal/ui/utils"
type CreateTransferProps struct {
SpaceID string
SourceAccountID string
SourceCurrency string
// DestAccounts is the list of other accounts in the same space the user
// can transfer to. Excludes the source account.
DestAccounts []*model.Account
// SourceAvailable / SourceAllocated are the source account's unallocated
// and allocated cash, formatted as plain decimal strings (e.g. "1234.50").
SourceAvailable string
SourceAllocated string
SourceOverflow bool
Title string
Amount string
DestAccountID string
ConversionRate string
Date string
Description string
TitleErr string
AmountErr string
DestErr string
RateErr string
DateErr string
GeneralErr string
}
templ CreateTransfer(props CreateTransferProps) {
<form hx-post={ routeurl.URL("action.app.spaces.space.accounts.account.transfers.create", "spaceID", props.SpaceID, "accountID", props.SourceAccountID) }>
@card.Card(card.Props{Class: "rounded-sm"}) {
@card.Content(card.ContentProps{Class: "p-4 space-y-4"}) {
if props.SourceAvailable != "" {
{{
availClasses := []string{"text-2xl font-bold"}
if props.SourceOverflow {
availClasses = append(availClasses, "text-red-600 dark:text-red-400")
}
availFormatted, _ := utils.FormatDecimalWithThousands(props.SourceAvailable)
allocFormatted, _ := utils.FormatDecimalWithThousands(props.SourceAllocated)
}}
<div class="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-2 border rounded-md p-4 bg-muted/30">
<div>
<p class="text-xs text-muted-foreground uppercase tracking-wide">Available</p>
<p class={ utils.TwMerge(availClasses...) }>${ availFormatted }</p>
</div>
<p class="text-sm text-muted-foreground">
Allocated: ${ allocFormatted }
</p>
</div>
}
if props.GeneralErr != "" {
@form.Message(form.MessageProps{Variant: form.MessageVariantError}) {
{ props.GeneralErr }
}
}
@form.Item() {
@form.Label(form.LabelProps{For: "title"}) {
Title
}
@input.Input(input.Props{
ID: "title",
Name: "title",
Type: input.TypeText,
Placeholder: "e.g. Move to savings",
Class: "rounded-sm",
Value: props.Title,
HasError: props.TitleErr != "",
Required: true,
Attributes: templ.Attributes{
"autocomplete": "off",
"autofocus": "",
},
})
if props.TitleErr != "" {
@form.Message(form.MessageProps{Variant: form.MessageVariantError}) {
{ props.TitleErr }
}
}
}
@form.Item() {
@form.Label(form.LabelProps{For: "destination"}) {
Destination account
}
if len(props.DestAccounts) == 0 {
@form.Message(form.MessageProps{Variant: form.MessageVariantError}) {
This space has no other accounts. Create one first.
}
} else {
<select
id="destination"
name="destination"
class={ "flex h-9 w-full items-center rounded-sm border bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
templ.KV("border-destructive", props.DestErr != ""),
templ.KV("border-input", props.DestErr == "") }
required
data-source-currency={ props.SourceCurrency }
_="on change
set opt to my.options[my.selectedIndex]
set destCur to opt.getAttribute('data-currency') or ''
set srcCur to @data-source-currency of me
if destCur is not '' and destCur is not srcCur
remove .hidden from #rate-row
set #rate-dest-currency.innerText to destCur
set #rate-source-currency.innerText to srcCur
else
add .hidden to #rate-row
end"
>
<option value="" selected?={ props.DestAccountID == "" }>Select an account…</option>
for _, a := range props.DestAccounts {
<option
value={ a.ID }
data-currency={ a.Currency }
selected?={ props.DestAccountID == a.ID }
>{ a.Name } ({ a.Currency })</option>
}
</select>
}
if props.DestErr != "" {
@form.Message(form.MessageProps{Variant: form.MessageVariantError}) {
{ props.DestErr }
}
}
}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@form.Item() {
@form.Label(form.LabelProps{For: "amount"}) {
Amount
}
@input.Input(input.Props{
ID: "amount",
Name: "amount",
Type: input.TypeNumber,
Placeholder: "0.00",
Class: "rounded-sm",
Value: props.Amount,
HasError: props.AmountErr != "",
Required: true,
Attributes: templ.Attributes{
"step": "0.01",
"min": "0",
"inputmode": "decimal",
"autocomplete": "off",
},
})
if props.AmountErr != "" {
@form.Message(form.MessageProps{Variant: form.MessageVariantError}) {
{ props.AmountErr }
}
}
@form.Description() {
Transfers may exceed the source balance and result in a negative balance.
}
}
@form.Item() {
@form.Label(form.LabelProps{For: "date"}) {
Date
}
@input.Input(input.Props{
ID: "date",
Name: "date",
Type: input.TypeDate,
Class: "rounded-sm",
Value: props.Date,
HasError: props.DateErr != "",
Required: true,
})
if props.DateErr != "" {
@form.Message(form.MessageProps{Variant: form.MessageVariantError}) {
{ props.DateErr }
}
}
}
</div>
{{
selectedDestCurrency := ""
for _, a := range props.DestAccounts {
if a.ID == props.DestAccountID {
selectedDestCurrency = a.Currency
break
}
}
rateRowHidden := selectedDestCurrency == "" || selectedDestCurrency == props.SourceCurrency
rateRowClasses := []string{"space-y-2 rounded-md border p-4 bg-muted/30"}
if rateRowHidden {
rateRowClasses = append(rateRowClasses, "hidden")
}
}}
<div id="rate-row" class={ utils.TwMerge(rateRowClasses...) }>
<p class="text-sm">
Source and destination use different currencies. Set the conversion rate.
</p>
@form.Item() {
@form.Label(form.LabelProps{For: "rate"}) {
Conversion rate
}
<div class="flex items-center gap-2">
<span class="text-sm text-muted-foreground">1 <span id="rate-source-currency">{ props.SourceCurrency }</span> =</span>
@input.Input(input.Props{
ID: "rate",
Name: "rate",
Type: input.TypeNumber,
Placeholder: "1.00",
Class: "rounded-sm",
Value: props.ConversionRate,
HasError: props.RateErr != "",
Attributes: templ.Attributes{
"step": "0.000001",
"min": "0",
"inputmode": "decimal",
"autocomplete": "off",
},
})
<span class="text-sm text-muted-foreground" id="rate-dest-currency">{ selectedDestCurrency }</span>
</div>
if props.RateErr != "" {
@form.Message(form.MessageProps{Variant: form.MessageVariantError}) {
{ props.RateErr }
}
}
@form.Description() {
The destination account will be credited the converted amount, rounded to 2 decimals.
}
}
</div>
@form.Item() {
@form.Label(form.LabelProps{For: "description"}) {
Description
}
@textarea.Textarea(textarea.Props{
ID: "description",
Name: "description",
Placeholder: "Anything extra worth remembering",
Rows: 3,
Value: props.Description,
})
@form.Description() {
Optional. Shared by both sides of the transfer.
}
}
}
@card.Footer(card.FooterProps{Class: "flex justify-end gap-2"}) {
@button.Button(button.Props{
Variant: button.VariantGhost,
Href: routeurl.URL("page.app.spaces.space.accounts.account.overview", "spaceID", props.SpaceID, "accountID", props.SourceAccountID),
}) {
Cancel
}
@button.Button(button.Props{
Type: button.TypeSubmit,
Disabled: len(props.DestAccounts) == 0,
}) {
Transfer
}
}
}
</form>
}