budgit/internal/ui/pages/app_space_loans.templ

235 lines
6.9 KiB
Text

package pages
import (
"fmt"
"strconv"
"github.com/shopspring/decimal"
"git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/card"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/csrf"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/dialog"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/form"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/pagination"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/progress"
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
)
templ SpaceLoansPage(space *model.Space, loans []*model.LoanWithPaymentSummary, currentPage, totalPages int) {
@layouts.Space("Loans", space) {
<div class="space-y-4">
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold">Loans</h1>
@dialog.Dialog(dialog.Props{}) {
@dialog.Trigger(dialog.TriggerProps{}) {
@button.Button(button.Props{Size: button.SizeSm}) {
@icon.Plus(icon.Props{Class: "size-4 mr-1"})
New Loan
}
}
@dialog.Content(dialog.ContentProps{}) {
@dialog.Header() {
@dialog.Title() {
New Loan
}
@dialog.Description() {
Track a new loan or financing
}
}
<form
hx-post={ fmt.Sprintf("/app/spaces/%s/loans", space.ID) }
hx-target="#loans-list-wrapper"
hx-swap="innerHTML"
_="on htmx:afterRequest if event.detail.successful call window.tui.dialog.close() then reset() me"
>
@csrf.Token()
@form.Item() {
@form.Label() {
Name
}
@input.Input(input.Props{
Type: input.TypeText,
Name: "name",
Placeholder: "e.g., Car Loan",
Attributes: templ.Attributes{"required": "true"},
})
}
@form.Item() {
@form.Label() {
Total Amount
}
@input.Input(input.Props{
Type: input.TypeNumber,
Name: "amount",
Placeholder: "0.00",
Attributes: templ.Attributes{
"step": "0.01",
"min": "0.01",
"required": "true",
},
})
}
@form.Item() {
@form.Label() {
Interest Rate (%)
}
@input.Input(input.Props{
Type: input.TypeNumber,
Name: "interest_rate",
Placeholder: "0.00",
Attributes: templ.Attributes{
"step": "0.01",
"min": "0",
},
})
}
@form.Item() {
@form.Label() {
Start Date
}
@input.Input(input.Props{
Type: input.TypeDate,
Name: "start_date",
Attributes: templ.Attributes{"required": "true"},
})
}
@form.Item() {
@form.Label() {
End Date (optional)
}
@input.Input(input.Props{
Type: input.TypeDate,
Name: "end_date",
})
}
@form.Item() {
@form.Label() {
Description (optional)
}
@input.Input(input.Props{
Type: input.TypeText,
Name: "description",
Placeholder: "Additional notes about this loan",
})
}
@dialog.Footer() {
@button.Button(button.Props{Type: "submit"}) {
Create Loan
}
}
</form>
}
}
</div>
<div id="loans-list-wrapper">
@LoansListContent(space.ID, loans, currentPage, totalPages)
</div>
</div>
}
}
templ LoansListContent(spaceID string, loans []*model.LoanWithPaymentSummary, currentPage, totalPages int) {
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
if len(loans) == 0 {
<p class="text-sm text-muted-foreground col-span-full">No loans yet. Create one to start tracking payments.</p>
}
for _, loan := range loans {
@LoanCard(spaceID, loan)
}
</div>
if totalPages > 1 {
<div class="mt-4">
@pagination.Pagination(pagination.Props{Class: "justify-center"}) {
@pagination.Content() {
@pagination.Item() {
@pagination.Previous(pagination.PreviousProps{
Disabled: currentPage <= 1,
Attributes: templ.Attributes{
"hx-get": fmt.Sprintf("/app/spaces/%s/loans?page=%d", spaceID, currentPage-1),
"hx-target": "#loans-list-wrapper",
"hx-swap": "innerHTML",
},
})
}
for _, pg := range pagination.CreatePagination(currentPage, totalPages, 3).Pages {
@pagination.Item() {
@pagination.Link(pagination.LinkProps{
IsActive: pg == currentPage,
Attributes: templ.Attributes{
"hx-get": fmt.Sprintf("/app/spaces/%s/loans?page=%d", spaceID, pg),
"hx-target": "#loans-list-wrapper",
"hx-swap": "innerHTML",
},
}) {
{ strconv.Itoa(pg) }
}
}
}
@pagination.Item() {
@pagination.Next(pagination.NextProps{
Disabled: currentPage >= totalPages,
Attributes: templ.Attributes{
"hx-get": fmt.Sprintf("/app/spaces/%s/loans?page=%d", spaceID, currentPage+1),
"hx-target": "#loans-list-wrapper",
"hx-swap": "innerHTML",
},
})
}
}
}
</div>
}
}
templ LoanCard(spaceID string, loan *model.LoanWithPaymentSummary) {
{{ progressPct := 0 }}
if !loan.OriginalAmount.IsZero() {
{{ progressPct = int(loan.TotalPaid.Div(loan.OriginalAmount).Mul(decimal.NewFromInt(100)).IntPart()) }}
if progressPct > 100 {
{{ progressPct = 100 }}
}
}
<a href={ templ.SafeURL(fmt.Sprintf("/app/spaces/%s/loans/%s", spaceID, loan.ID)) } class="block">
@card.Card(card.Props{Class: "hover:border-primary/50 transition-colors cursor-pointer"}) {
@card.Header() {
<div class="flex justify-between items-start">
@card.Title() {
{ loan.Name }
}
if loan.IsPaidOff {
@badge.Badge(badge.Props{Variant: badge.VariantDefault}) {
Paid Off
}
}
</div>
@card.Description() {
{ model.FormatMoney(loan.OriginalAmount) }
if loan.InterestRateBps > 0 {
{ fmt.Sprintf(" @ %.2f%%", float64(loan.InterestRateBps)/100.0) }
}
}
}
@card.Content() {
@progress.Progress(progress.Props{
Value: progressPct,
Max: 100,
Class: "h-2",
})
<div class="flex justify-between text-sm text-muted-foreground mt-2">
<span>Paid: { model.FormatMoney(loan.TotalPaid) }</span>
if loan.Remaining.GreaterThan(decimal.Zero) {
<span>Left: { model.FormatMoney(loan.Remaining) }</span>
} else {
<span class="text-green-600">Fully paid</span>
}
</div>
<p class="text-xs text-muted-foreground mt-1">
{ strconv.Itoa(loan.ReceiptCount) } payment(s)
</p>
}
}
</a>
}