feat(ui): choose how a late payday is priced
Surfaces the pricing modes in the console: a selector plus a rate field on
both the create and edit dialogs, and a "Rate used" column in the ledger
showing the figure alongside where it came from, so a payout can be
explained without opening the database.
Pay-now becomes a dialog rather than a confirm. That is the moment an
operator is most likely to want a rate other than the contract's own —
entering a payment that happened weeks ago at a figure they already know —
and the choice has to be made before it settles, not after. It defaults to
the contract's mode, disables the rate field unless "Rate I enter" is
picked, and refuses to submit a manual mode with no rate. The success toast
now names the rate applied, and a refused payout still shows its reason.
The mode selector is disabled for sat-denominated contracts, where there is
no conversion to have an opinion about, and its hint says the setting only
applies to a payday already in the past — otherwise it reads as though it
governs every payout, which it does not.
Quasar UMD rules honoured: no self-closing tags, `${ }` delimiters,
`:style` bindings rather than a <style> block.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj
This commit is contained in:
parent
18a17cd693
commit
3504bf2b02
3 changed files with 179 additions and 29 deletions
|
|
@ -15,6 +15,12 @@ const FREQUENCIES = [
|
|||
{value: 'yearly', label: 'Yearly'}
|
||||
]
|
||||
|
||||
const PRICING_MODES = [
|
||||
{value: 'payday', label: "Rate on the payday"},
|
||||
{value: 'current', label: "Rate at settlement"},
|
||||
{value: 'manual', label: 'Rate I enter'}
|
||||
]
|
||||
|
||||
const STATUS_COLOUR = {
|
||||
active: 'positive',
|
||||
paused: 'warning',
|
||||
|
|
@ -45,7 +51,11 @@ function emptyContract() {
|
|||
total_periods: 12,
|
||||
label: '',
|
||||
memo: '',
|
||||
backfill: false
|
||||
backfill: false,
|
||||
// Only ever consulted for a payday in the past; for a payday today or
|
||||
// ahead every mode collapses to LNbits' own live pricing.
|
||||
pricing_mode: 'payday',
|
||||
manual_rate: null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +79,14 @@ window.app = Vue.createApp({
|
|||
frequencyOptions: FREQUENCIES,
|
||||
|
||||
contractDialog: {show: false, data: emptyContract(), saving: false},
|
||||
payNowDialog: {
|
||||
show: false,
|
||||
contract: null,
|
||||
pricing_mode: 'payday',
|
||||
manual_rate: null,
|
||||
loading: false
|
||||
},
|
||||
pricingModes: PRICING_MODES,
|
||||
// Live calendar of whatever is currently typed into the dialog. The
|
||||
// cheapest moment to notice a wrong start date or frequency is before
|
||||
// anything is saved.
|
||||
|
|
@ -121,6 +139,7 @@ window.app = Vue.createApp({
|
|||
align: 'left',
|
||||
field: 'contract_id'
|
||||
},
|
||||
{name: 'rate', label: 'Rate used', align: 'right', field: 'rate'},
|
||||
{name: 'attempt', label: 'Try', align: 'center', field: 'attempt'},
|
||||
{name: 'detail', label: 'Detail', align: 'left', field: 'detail'}
|
||||
]
|
||||
|
|
@ -331,7 +350,9 @@ window.app = Vue.createApp({
|
|||
total_periods: d.open_ended ? null : Number(d.total_periods),
|
||||
label: d.label,
|
||||
memo: d.memo,
|
||||
backfill: d.backfill
|
||||
backfill: d.backfill,
|
||||
pricing_mode: d.pricing_mode,
|
||||
manual_rate: d.pricing_mode === 'manual' ? Number(d.manual_rate) : null
|
||||
})
|
||||
this.contractDialog.show = false
|
||||
Quasar.Notify.create({type: 'positive', message: 'Payroll line created'})
|
||||
|
|
@ -354,7 +375,9 @@ window.app = Vue.createApp({
|
|||
open_ended: contract.total_periods === null,
|
||||
total_periods: contract.total_periods,
|
||||
label: contract.label,
|
||||
memo: contract.memo
|
||||
memo: contract.memo,
|
||||
pricing_mode: contract.pricing_mode,
|
||||
manual_rate: contract.manual_rate
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -372,7 +395,9 @@ window.app = Vue.createApp({
|
|||
frequency: d.frequency,
|
||||
total_periods: d.open_ended ? null : Number(d.total_periods),
|
||||
label: d.label,
|
||||
memo: d.memo
|
||||
memo: d.memo,
|
||||
pricing_mode: d.pricing_mode,
|
||||
manual_rate: d.pricing_mode === 'manual' ? Number(d.manual_rate) : null
|
||||
}
|
||||
)
|
||||
this.editDialog.show = false
|
||||
|
|
@ -416,33 +441,49 @@ window.app = Vue.createApp({
|
|||
},
|
||||
|
||||
payNow(contract) {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
`Pay the next period of "${contract.label || contract.id}" now? ` +
|
||||
'It is consumed even if its payday has not arrived yet.'
|
||||
// A dialog rather than a confirm: paying a back-dated period is
|
||||
// exactly when the operator may want a different rate than the
|
||||
// contract's own, and that choice has to be made before it settles.
|
||||
this.payNowDialog = {
|
||||
show: true,
|
||||
contract,
|
||||
pricing_mode: contract.pricing_mode || 'payday',
|
||||
manual_rate: contract.manual_rate,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
|
||||
async confirmPayNow() {
|
||||
const d = this.payNowDialog
|
||||
const params = new URLSearchParams({pricing_mode: d.pricing_mode})
|
||||
if (d.pricing_mode === 'manual' && d.manual_rate) {
|
||||
params.append('manual_rate', String(d.manual_rate))
|
||||
}
|
||||
d.loading = true
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
`${API}/contracts/${d.contract.id}/pay-now?${params.toString()}`
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
`${API}/contracts/${contract.id}/pay-now`
|
||||
)
|
||||
// A refused payout still returns a ledger row — surfacing its
|
||||
// reason is the whole point of triggering one by hand.
|
||||
Quasar.Notify.create({
|
||||
type: data.status === 'paid' ? 'positive' : 'negative',
|
||||
message:
|
||||
data.status === 'paid'
|
||||
? `Paid ${Math.round(data.amount_msat / 1000)} sat`
|
||||
: `Payout ${data.status}: ${data.detail}`,
|
||||
timeout: data.status === 'paid' ? 3000 : 8000
|
||||
})
|
||||
this.getContracts()
|
||||
this.getPayouts()
|
||||
} catch (err) {
|
||||
this._err(err, 'Could not pay now')
|
||||
}
|
||||
// A refused payout still returns a ledger row — surfacing its
|
||||
// reason is the whole point of triggering one by hand.
|
||||
Quasar.Notify.create({
|
||||
type: data.status === 'paid' ? 'positive' : 'negative',
|
||||
message:
|
||||
data.status === 'paid'
|
||||
? `Paid ${Math.round(data.amount_msat / 1000)} sat` +
|
||||
(data.rate ? ` at ${data.rate.toLocaleString()} / BTC` : '')
|
||||
: `Payout ${data.status}: ${data.detail}`,
|
||||
timeout: data.status === 'paid' ? 4000 : 9000
|
||||
})
|
||||
d.show = false
|
||||
this.getContracts()
|
||||
this.getPayouts()
|
||||
} catch (err) {
|
||||
this._err(err, 'Could not pay now')
|
||||
} finally {
|
||||
d.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
deleteContract(contract) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue