608 lines
18 KiB
Text
608 lines
18 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/badge"
|
|
"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/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 SpaceLoanDetailPage(space *model.Space, loan *model.LoanWithPaymentSummary, receipts []*model.ReceiptWithSourcesAndAccounts, currentPage, totalPages int, recurringReceipts []*model.RecurringReceiptWithSources, accounts []model.MoneyAccountWithBalance, availableBalance decimal.Decimal) {
|
|
@layouts.Space(loan.Name, space) {
|
|
<div class="space-y-6">
|
|
// Loan Summary Card
|
|
@LoanSummaryCard(space.ID, loan)
|
|
|
|
// Actions
|
|
if !loan.IsPaidOff {
|
|
<div class="flex gap-2">
|
|
@dialog.Dialog(dialog.Props{}) {
|
|
@dialog.Trigger(dialog.TriggerProps{}) {
|
|
@button.Button(button.Props{Size: button.SizeSm}) {
|
|
@icon.Plus(icon.Props{Class: "size-4 mr-1"})
|
|
Make Payment
|
|
}
|
|
}
|
|
@dialog.Content(dialog.ContentProps{Class: "max-w-lg"}) {
|
|
@dialog.Header() {
|
|
@dialog.Title() {
|
|
Make Payment
|
|
}
|
|
@dialog.Description() {
|
|
Record a payment toward { loan.Name }
|
|
}
|
|
}
|
|
@CreateReceiptForm(space.ID, loan.ID, accounts, availableBalance)
|
|
}
|
|
}
|
|
@dialog.Dialog(dialog.Props{}) {
|
|
@dialog.Trigger(dialog.TriggerProps{}) {
|
|
@button.Button(button.Props{Size: button.SizeSm, Variant: button.VariantOutline}) {
|
|
@icon.Repeat(icon.Props{Class: "size-4 mr-1"})
|
|
Set Up Recurring
|
|
}
|
|
}
|
|
@dialog.Content(dialog.ContentProps{Class: "max-w-lg"}) {
|
|
@dialog.Header() {
|
|
@dialog.Title() {
|
|
Recurring Payment
|
|
}
|
|
@dialog.Description() {
|
|
Automatically create payments on a schedule
|
|
}
|
|
}
|
|
@CreateRecurringReceiptForm(space.ID, loan.ID, accounts, availableBalance)
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// Recurring Receipts
|
|
if len(recurringReceipts) > 0 {
|
|
<div class="space-y-2">
|
|
<h2 class="text-lg font-semibold">Recurring Payments</h2>
|
|
<div class="border rounded-lg divide-y">
|
|
for _, rr := range recurringReceipts {
|
|
@RecurringReceiptItem(space.ID, loan.ID, rr)
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// Receipt History
|
|
<div class="space-y-2">
|
|
<h2 class="text-lg font-semibold">Payment History</h2>
|
|
<div class="border rounded-lg">
|
|
<div id="receipts-list-wrapper">
|
|
@ReceiptsListContent(space.ID, loan.ID, receipts, currentPage, totalPages)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
templ LoanSummaryCard(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 }}
|
|
}
|
|
}
|
|
@card.Card(card.Props{}) {
|
|
@card.Header() {
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
@card.Title() {
|
|
{ loan.Name }
|
|
}
|
|
@card.Description() {
|
|
if loan.Description != "" {
|
|
{ loan.Description }
|
|
}
|
|
}
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
if loan.IsPaidOff {
|
|
@badge.Badge(badge.Props{Variant: badge.VariantDefault}) {
|
|
Paid Off
|
|
}
|
|
}
|
|
@dialog.Dialog(dialog.Props{}) {
|
|
@dialog.Trigger(dialog.TriggerProps{}) {
|
|
@button.Button(button.Props{Size: button.SizeIcon, Variant: button.VariantGhost}) {
|
|
@icon.Trash2(icon.Props{Class: "size-4"})
|
|
}
|
|
}
|
|
@dialog.Content(dialog.ContentProps{}) {
|
|
@dialog.Header() {
|
|
@dialog.Title() {
|
|
Delete Loan
|
|
}
|
|
@dialog.Description() {
|
|
Are you sure? This will delete all payment records for this loan. Linked expenses and account transfers will be kept as history.
|
|
}
|
|
}
|
|
@dialog.Footer() {
|
|
@button.Button(button.Props{
|
|
Variant: button.VariantDestructive,
|
|
Attributes: templ.Attributes{
|
|
"hx-delete": fmt.Sprintf("/app/spaces/%s/loans/%s", spaceID, loan.ID),
|
|
},
|
|
}) {
|
|
Delete
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
@card.Content() {
|
|
<div class="space-y-4">
|
|
<div class="grid grid-cols-3 gap-4 text-center">
|
|
<div>
|
|
<p class="text-sm text-muted-foreground">Original</p>
|
|
<p class="text-lg font-semibold">{ model.FormatMoney(loan.OriginalAmount) }</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-muted-foreground">Paid</p>
|
|
<p class="text-lg font-semibold text-green-600">{ model.FormatMoney(loan.TotalPaid) }</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-muted-foreground">Remaining</p>
|
|
<p class="text-lg font-semibold">
|
|
if loan.Remaining.GreaterThan(decimal.Zero) {
|
|
{ model.FormatMoney(loan.Remaining) }
|
|
} else {
|
|
$0.00
|
|
}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
@progress.Progress(progress.Props{
|
|
Value: progressPct,
|
|
Max: 100,
|
|
Class: "h-3",
|
|
})
|
|
<div class="flex justify-between text-sm text-muted-foreground">
|
|
<span>{ strconv.Itoa(progressPct) }% paid</span>
|
|
if loan.InterestRateBps > 0 {
|
|
<span>{ fmt.Sprintf("%.2f%% interest", float64(loan.InterestRateBps)/100.0) }</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
|
|
templ ReceiptsListContent(spaceID, loanID string, receipts []*model.ReceiptWithSourcesAndAccounts, currentPage, totalPages int) {
|
|
<div id="receipts-list" class="divide-y">
|
|
if len(receipts) == 0 {
|
|
<p class="p-4 text-sm text-muted-foreground">No payments recorded yet.</p>
|
|
}
|
|
for _, receipt := range receipts {
|
|
@ReceiptListItem(spaceID, loanID, receipt)
|
|
}
|
|
</div>
|
|
if totalPages > 1 {
|
|
<div class="border-t p-2">
|
|
@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/%s/components/receipts?page=%d", spaceID, loanID, currentPage-1),
|
|
"hx-target": "#receipts-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/%s/components/receipts?page=%d", spaceID, loanID, pg),
|
|
"hx-target": "#receipts-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/%s/components/receipts?page=%d", spaceID, loanID, currentPage+1),
|
|
"hx-target": "#receipts-list-wrapper",
|
|
"hx-swap": "innerHTML",
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
|
|
templ ReceiptListItem(spaceID, loanID string, receipt *model.ReceiptWithSourcesAndAccounts) {
|
|
<div id={ "receipt-" + receipt.ID } class="p-4 flex justify-between items-start">
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-medium">{ model.FormatMoney(receipt.TotalAmount) }</span>
|
|
<span class="text-sm text-muted-foreground">{ receipt.Date.Format("Jan 2, 2006") }</span>
|
|
if receipt.RecurringReceiptID != nil {
|
|
@icon.Repeat(icon.Props{Class: "size-3 text-muted-foreground"})
|
|
}
|
|
</div>
|
|
if receipt.Description != "" {
|
|
<p class="text-sm text-muted-foreground">{ receipt.Description }</p>
|
|
}
|
|
<div class="flex flex-wrap gap-1">
|
|
for _, src := range receipt.Sources {
|
|
if src.SourceType == "balance" {
|
|
@badge.Badge(badge.Props{Variant: badge.VariantSecondary, Class: "text-xs"}) {
|
|
{ fmt.Sprintf("Balance %s", model.FormatMoney(src.Amount)) }
|
|
}
|
|
} else {
|
|
@badge.Badge(badge.Props{Variant: badge.VariantOutline, Class: "text-xs"}) {
|
|
{ fmt.Sprintf("%s %s", src.AccountName, model.FormatMoney(src.Amount)) }
|
|
}
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
@dialog.Dialog(dialog.Props{}) {
|
|
@dialog.Trigger(dialog.TriggerProps{}) {
|
|
@button.Button(button.Props{Size: button.SizeIcon, Variant: button.VariantGhost}) {
|
|
@icon.Trash2(icon.Props{Class: "size-4"})
|
|
}
|
|
}
|
|
@dialog.Content(dialog.ContentProps{}) {
|
|
@dialog.Header() {
|
|
@dialog.Title() {
|
|
Delete Payment
|
|
}
|
|
@dialog.Description() {
|
|
This will also reverse the linked expense and account transfers.
|
|
}
|
|
}
|
|
@dialog.Footer() {
|
|
@button.Button(button.Props{
|
|
Variant: button.VariantDestructive,
|
|
Attributes: templ.Attributes{
|
|
"hx-delete": fmt.Sprintf("/app/spaces/%s/loans/%s/receipts/%s", spaceID, loanID, receipt.ID),
|
|
},
|
|
}) {
|
|
Delete
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ RecurringReceiptItem(spaceID, loanID string, rr *model.RecurringReceiptWithSources) {
|
|
<div class="p-4 flex justify-between items-start">
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-2">
|
|
@icon.Repeat(icon.Props{Class: "size-4"})
|
|
<span class="font-medium">{ model.FormatMoney(rr.TotalAmount) }</span>
|
|
<span class="text-sm text-muted-foreground">{ string(rr.Frequency) }</span>
|
|
if !rr.IsActive {
|
|
@badge.Badge(badge.Props{Variant: badge.VariantSecondary}) {
|
|
Paused
|
|
}
|
|
}
|
|
</div>
|
|
if rr.Description != "" {
|
|
<p class="text-sm text-muted-foreground">{ rr.Description }</p>
|
|
}
|
|
<p class="text-xs text-muted-foreground">
|
|
Next: { rr.NextOccurrence.Format("Jan 2, 2006") }
|
|
</p>
|
|
<div class="flex flex-wrap gap-1">
|
|
for _, src := range rr.Sources {
|
|
if src.SourceType == "balance" {
|
|
@badge.Badge(badge.Props{Variant: badge.VariantSecondary, Class: "text-xs"}) {
|
|
{ fmt.Sprintf("Balance %s", model.FormatMoney(src.Amount)) }
|
|
}
|
|
} else {
|
|
@badge.Badge(badge.Props{Variant: badge.VariantOutline, Class: "text-xs"}) {
|
|
if src.AccountID != nil {
|
|
{ fmt.Sprintf("Account %s", model.FormatMoney(src.Amount)) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
@button.Button(button.Props{
|
|
Size: button.SizeIcon,
|
|
Variant: button.VariantGhost,
|
|
Attributes: templ.Attributes{
|
|
"hx-post": fmt.Sprintf("/app/spaces/%s/loans/%s/recurring/%s/toggle", spaceID, loanID, rr.ID),
|
|
},
|
|
}) {
|
|
if rr.IsActive {
|
|
@icon.Pause(icon.Props{Class: "size-4"})
|
|
} else {
|
|
@icon.Play(icon.Props{Class: "size-4"})
|
|
}
|
|
}
|
|
@dialog.Dialog(dialog.Props{}) {
|
|
@dialog.Trigger(dialog.TriggerProps{}) {
|
|
@button.Button(button.Props{Size: button.SizeIcon, Variant: button.VariantGhost}) {
|
|
@icon.Trash2(icon.Props{Class: "size-4"})
|
|
}
|
|
}
|
|
@dialog.Content(dialog.ContentProps{}) {
|
|
@dialog.Header() {
|
|
@dialog.Title() {
|
|
Delete Recurring Payment
|
|
}
|
|
@dialog.Description() {
|
|
This will stop future automatic payments. Past payments are not affected.
|
|
}
|
|
}
|
|
@dialog.Footer() {
|
|
@button.Button(button.Props{
|
|
Variant: button.VariantDestructive,
|
|
Attributes: templ.Attributes{
|
|
"hx-delete": fmt.Sprintf("/app/spaces/%s/loans/%s/recurring/%s", spaceID, loanID, rr.ID),
|
|
},
|
|
}) {
|
|
Delete
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ CreateReceiptForm(spaceID, loanID string, accounts []model.MoneyAccountWithBalance, availableBalance decimal.Decimal) {
|
|
<form
|
|
hx-post={ fmt.Sprintf("/app/spaces/%s/loans/%s/receipts", spaceID, loanID) }
|
|
hx-swap="none"
|
|
>
|
|
@csrf.Token()
|
|
@form.Item() {
|
|
@form.Label() {
|
|
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() {
|
|
Date
|
|
}
|
|
@input.Input(input.Props{
|
|
Type: input.TypeDate,
|
|
Name: "date",
|
|
Attributes: templ.Attributes{"required": "true"},
|
|
})
|
|
}
|
|
@form.Item() {
|
|
@form.Label() {
|
|
Description (optional)
|
|
}
|
|
@input.Input(input.Props{
|
|
Type: input.TypeText,
|
|
Name: "description",
|
|
Placeholder: "Payment note",
|
|
})
|
|
}
|
|
// Funding Sources
|
|
<div class="space-y-2">
|
|
<label class="text-sm font-medium">Funding Sources</label>
|
|
<p class="text-xs text-muted-foreground">
|
|
Available balance: { model.FormatMoney(availableBalance) }
|
|
</p>
|
|
<div id="funding-sources" class="space-y-2">
|
|
<div class="flex gap-2 items-center source-row">
|
|
<select name="source_type" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm">
|
|
<option value="balance">General Balance</option>
|
|
for _, acct := range accounts {
|
|
<option value="account" data-account-id={ acct.ID }>
|
|
{ acct.Name } ({ model.FormatMoney(acct.Balance) })
|
|
</option>
|
|
}
|
|
</select>
|
|
<input type="hidden" name="source_account_id" value=""/>
|
|
@input.Input(input.Props{
|
|
Type: input.TypeNumber,
|
|
Name: "source_amount",
|
|
Placeholder: "0.00",
|
|
Attributes: templ.Attributes{
|
|
"step": "0.01",
|
|
"min": "0.01", "required": "true",
|
|
},
|
|
})
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="text-sm text-primary hover:underline"
|
|
_="on click
|
|
set row to the first .source-row
|
|
set clone to row.cloneNode(true)
|
|
put '' into the value of the first <select/> in clone
|
|
put '' into the value of the first <input[type='hidden']/> in clone
|
|
put '' into the value of the first <input[type='number']/> in clone
|
|
append clone to #funding-sources"
|
|
>
|
|
+ Add Source
|
|
</button>
|
|
</div>
|
|
@dialog.Footer() {
|
|
@button.Button(button.Props{Type: "submit"}) {
|
|
Record Payment
|
|
}
|
|
}
|
|
</form>
|
|
<script>
|
|
// Update hidden account_id when select changes
|
|
document.getElementById('funding-sources').addEventListener('change', function(e) {
|
|
if (e.target.tagName === 'SELECT') {
|
|
const selected = e.target.options[e.target.selectedIndex];
|
|
const hiddenInput = e.target.parentElement.querySelector('input[type="hidden"]');
|
|
if (selected.value === 'account') {
|
|
hiddenInput.value = selected.dataset.accountId || '';
|
|
} else {
|
|
hiddenInput.value = '';
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
}
|
|
|
|
templ CreateRecurringReceiptForm(spaceID, loanID string, accounts []model.MoneyAccountWithBalance, availableBalance decimal.Decimal) {
|
|
<form
|
|
hx-post={ fmt.Sprintf("/app/spaces/%s/loans/%s/recurring", spaceID, loanID) }
|
|
hx-swap="none"
|
|
>
|
|
@csrf.Token()
|
|
@form.Item() {
|
|
@form.Label() {
|
|
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() {
|
|
Frequency
|
|
}
|
|
<select name="frequency" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm" required>
|
|
<option value="monthly">Monthly</option>
|
|
<option value="weekly">Weekly</option>
|
|
<option value="biweekly">Biweekly</option>
|
|
<option value="yearly">Yearly</option>
|
|
</select>
|
|
}
|
|
@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: "Payment note",
|
|
})
|
|
}
|
|
// Funding Sources
|
|
<div class="space-y-2">
|
|
<label class="text-sm font-medium">Funding Sources</label>
|
|
<p class="text-xs text-muted-foreground">
|
|
Current balance: { model.FormatMoney(availableBalance) }
|
|
</p>
|
|
<div id="recurring-funding-sources" class="space-y-2">
|
|
<div class="flex gap-2 items-center recurring-source-row">
|
|
<select name="source_type" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm">
|
|
<option value="balance">General Balance</option>
|
|
for _, acct := range accounts {
|
|
<option value="account" data-account-id={ acct.ID }>
|
|
{ acct.Name } ({ model.FormatMoney(acct.Balance) })
|
|
</option>
|
|
}
|
|
</select>
|
|
<input type="hidden" name="source_account_id" value=""/>
|
|
@input.Input(input.Props{
|
|
Type: input.TypeNumber,
|
|
Name: "source_amount",
|
|
Placeholder: "0.00",
|
|
Attributes: templ.Attributes{
|
|
"step": "0.01",
|
|
"min": "0.01", "required": "true",
|
|
},
|
|
})
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="text-sm text-primary hover:underline"
|
|
_="on click
|
|
set row to the first .recurring-source-row
|
|
set clone to row.cloneNode(true)
|
|
put '' into the value of the first <select/> in clone
|
|
put '' into the value of the first <input[type='hidden']/> in clone
|
|
put '' into the value of the first <input[type='number']/> in clone
|
|
append clone to #recurring-funding-sources"
|
|
>
|
|
+ Add Source
|
|
</button>
|
|
</div>
|
|
@dialog.Footer() {
|
|
@button.Button(button.Props{Type: "submit"}) {
|
|
Create Recurring Payment
|
|
}
|
|
}
|
|
</form>
|
|
<script>
|
|
document.getElementById('recurring-funding-sources').addEventListener('change', function(e) {
|
|
if (e.target.tagName === 'SELECT') {
|
|
const selected = e.target.options[e.target.selectedIndex];
|
|
const hiddenInput = e.target.parentElement.querySelector('input[type="hidden"]');
|
|
if (selected.value === 'account') {
|
|
hiddenInput.value = selected.dataset.accountId || '';
|
|
} else {
|
|
hiddenInput.value = '';
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
}
|