Enables equity eligibility for users

Allows superusers to grant and revoke equity eligibility for users.
Adds UI components for managing equity eligibility.
Equity-eligible users can then contribute expenses as equity.
This commit is contained in:
padreug 2025-11-08 10:14:24 +01:00
parent 33c294de7f
commit eefabc3441
4 changed files with 338 additions and 1 deletions

View file

@ -15,6 +15,7 @@ window.app = Vue.createApp({
users: [],
settings: null,
userWalletSettings: null,
userInfo: null, // User information including equity eligibility
isAdmin: false,
isSuperUser: false,
castleWalletConfigured: false,
@ -353,6 +354,19 @@ window.app = Vue.createApp({
console.error('Error loading users:', error)
}
},
async loadUserInfo() {
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/user/info',
this.g.user.wallets[0].inkey
)
this.userInfo = response.data
} catch (error) {
console.error('Error loading user info:', error)
this.userInfo = { is_equity_eligible: false }
}
},
async loadSettings() {
try {
// Try with admin key first to check settings
@ -1457,6 +1471,7 @@ window.app = Vue.createApp({
// Load settings first to determine if user is super user
await this.loadSettings()
await this.loadUserWallet()
await this.loadUserInfo()
await this.loadExchangeRate()
await this.loadBalance()
await this.loadTransactions()

View file

@ -7,13 +7,19 @@ window.app = Vue.createApp({
accounts: [],
users: [],
filteredUsers: [],
equityEligibleUsers: [],
loading: false,
granting: false,
revoking: false,
grantingEquity: false,
revokingEquity: false,
activeTab: 'by-user',
showGrantDialog: false,
showRevokeDialog: false,
showGrantEquityDialog: false,
showRevokeEquityDialog: false,
permissionToRevoke: null,
equityToRevoke: null,
isSuperUser: false,
grantForm: {
user_id: '',
@ -22,6 +28,10 @@ window.app = Vue.createApp({
notes: '',
expires_at: ''
},
grantEquityForm: {
user_id: '',
notes: ''
},
permissionTypeOptions: [
{
value: 'read',
@ -326,6 +336,124 @@ window.app = Vue.createApp({
if (!dateString) return '-'
const date = new Date(dateString)
return date.toLocaleString()
},
async loadEquityEligibleUsers() {
if (!this.isSuperUser) {
return
}
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/admin/equity-eligibility',
this.g.user.wallets[0].adminkey
)
this.equityEligibleUsers = response.data || []
} catch (error) {
console.error('Failed to load equity-eligible users:', error)
this.$q.notify({
type: 'negative',
message: 'Failed to load equity-eligible users',
caption: error.message || 'Unknown error',
timeout: 5000
})
}
},
async grantEquityEligibility() {
if (!this.grantEquityForm.user_id) {
this.$q.notify({
type: 'warning',
message: 'Please select a user',
timeout: 3000
})
return
}
this.grantingEquity = true
try {
const payload = {
user_id: this.grantEquityForm.user_id,
is_equity_eligible: true
}
if (this.grantEquityForm.notes) {
payload.notes = this.grantEquityForm.notes
}
await LNbits.api.request(
'POST',
'/castle/api/v1/admin/equity-eligibility',
this.g.user.wallets[0].adminkey,
payload
)
this.$q.notify({
type: 'positive',
message: 'Equity eligibility granted successfully',
timeout: 3000
})
this.showGrantEquityDialog = false
this.resetGrantEquityForm()
await this.loadEquityEligibleUsers()
} catch (error) {
console.error('Failed to grant equity eligibility:', error)
this.$q.notify({
type: 'negative',
message: 'Failed to grant equity eligibility',
caption: error.message || 'Unknown error',
timeout: 5000
})
} finally {
this.grantingEquity = false
}
},
confirmRevokeEquity(equity) {
this.equityToRevoke = equity
this.showRevokeEquityDialog = true
},
async revokeEquityEligibility() {
if (!this.equityToRevoke) return
this.revokingEquity = true
try {
await LNbits.api.request(
'DELETE',
`/castle/api/v1/admin/equity-eligibility/${this.equityToRevoke.user_id}`,
this.g.user.wallets[0].adminkey
)
this.$q.notify({
type: 'positive',
message: 'Equity eligibility revoked successfully',
timeout: 3000
})
this.showRevokeEquityDialog = false
this.equityToRevoke = null
await this.loadEquityEligibleUsers()
} catch (error) {
console.error('Failed to revoke equity eligibility:', error)
this.$q.notify({
type: 'negative',
message: 'Failed to revoke equity eligibility',
caption: error.message || 'Unknown error',
timeout: 5000
})
} finally {
this.revokingEquity = false
}
},
resetGrantEquityForm() {
this.grantEquityForm = {
user_id: '',
notes: ''
}
}
},
@ -338,7 +466,8 @@ window.app = Vue.createApp({
if (this.isSuperUser) {
await Promise.all([
this.loadPermissions(),
this.loadUsers()
this.loadUsers(),
this.loadEquityEligibleUsers()
])
}
}

View file

@ -723,6 +723,7 @@
></q-select>
<q-select
v-if="userInfo && userInfo.is_equity_eligible"
filled
dense
v-model="expenseDialog.isEquity"
@ -735,8 +736,25 @@
emit-value
map-options
label="Type *"
hint="Choose whether this is a liability (Castle owes you) or an equity contribution"
></q-select>
<!-- If user is not equity eligible, force liability -->
<div v-else>
<q-input
filled
dense
readonly
:model-value="'Liability (Castle owes me)'"
label="Type"
hint="This expense will be recorded as a liability (Castle owes you)"
>
<template v-slot:prepend>
<q-icon name="info" color="blue-grey-7"></q-icon>
</template>
</q-input>
</div>
<q-input
filled
dense

View file

@ -44,6 +44,7 @@
<q-tabs v-model="activeTab" class="text-primary" dense>
<q-tab name="by-user" icon="people" label="By User"></q-tab>
<q-tab name="by-account" icon="account_balance" label="By Account"></q-tab>
<q-tab name="equity" icon="account_balance_wallet" label="Equity Eligibility"></q-tab>
</q-tabs>
<q-separator></q-separator>
@ -170,6 +171,68 @@
</q-card>
</div>
</q-tab-panel>
<!-- Equity Eligibility View -->
<q-tab-panel name="equity">
<div v-if="loading" class="row justify-center q-pa-md">
<q-spinner color="primary" size="3em"></q-spinner>
</div>
<div class="row q-mb-md">
<div class="col">
<q-btn
color="primary"
icon="add"
label="Grant Equity Eligibility"
@click="showGrantEquityDialog = true"
:disable="!isSuperUser"
>
<q-tooltip v-if="!isSuperUser">Admin access required</q-tooltip>
</q-btn>
</div>
</div>
<div v-if="equityEligibleUsers.length === 0" class="text-center q-pa-md">
<q-icon name="info" size="3em" color="grey-5"></q-icon>
<p class="text-grey-6">No equity-eligible users yet</p>
</div>
<div v-else class="q-gutter-md">
<q-card v-for="equity in equityEligibleUsers" :key="equity.user_id" flat bordered>
<q-card-section>
<div class="row items-center">
<div class="col">
<div class="text-h6">
<q-icon name="person" class="q-mr-sm"></q-icon>
{% raw %}{{ equity.user_id }}{% endraw %}
</div>
<div class="text-caption text-grey-7 q-mt-sm">
<strong>Equity Account:</strong> {% raw %}{{ equity.equity_account_name }}{% endraw %}
</div>
<div v-if="equity.notes" class="text-caption text-grey-7 q-mt-xs">
<strong>Notes:</strong> {% raw %}{{ equity.notes }}{% endraw %}
</div>
<div class="text-caption text-grey-6 q-mt-xs">
Granted: {% raw %}{{ formatDate(equity.granted_at) }}{% endraw %}
</div>
</div>
<div class="col-auto">
<q-btn
flat
round
dense
icon="delete"
color="negative"
@click="confirmRevokeEquity(equity)"
>
<q-tooltip>Revoke Equity Eligibility</q-tooltip>
</q-btn>
</div>
</div>
</q-card-section>
</q-card>
</div>
</q-tab-panel>
</q-tab-panels>
</q-card-section>
</q-card>
@ -348,4 +411,116 @@
</q-card-actions>
</q-card>
</q-dialog>
<!-- Grant Equity Eligibility Dialog -->
<q-dialog v-model="showGrantEquityDialog" position="top">
<q-card class="q-pa-lg" style="min-width: 500px">
<q-card-section>
<div class="text-h6">Grant Equity Eligibility</div>
<div class="text-caption text-grey-7">
Grant a user the ability to contribute expenses as equity instead of liability.
An equity account will be automatically created for this user.
</div>
</q-card-section>
<q-card-section class="q-gutter-md">
<!-- User -->
<q-select
v-model="grantEquityForm.user_id"
label="User *"
hint="Search and select a user"
:options="userOptions"
option-value="id"
option-label="label"
emit-value
map-options
use-input
@filter="filterUsers"
outlined
dense
:rules="[val => !!val || 'User is required']"
>
<template v-slot:prepend>
<q-icon name="person"></q-icon>
</template>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
No users found
</q-item-section>
</q-item>
</template>
</q-select>
<!-- Notes (Optional) -->
<q-input
v-model="grantEquityForm.notes"
label="Notes (Optional)"
hint="Optional notes for admin reference"
type="textarea"
outlined
dense
rows="3"
>
<template v-slot:prepend>
<q-icon name="note"></q-icon>
</template>
</q-input>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="grey" v-close-popup></q-btn>
<q-btn
unelevated
label="Grant Equity Eligibility"
color="primary"
@click="grantEquityEligibility"
:loading="grantingEquity"
:disable="!grantEquityForm.user_id"
></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
<!-- Revoke Equity Confirmation Dialog -->
<q-dialog v-model="showRevokeEquityDialog" persistent>
<q-card>
<q-card-section>
<div class="text-h6">Revoke Equity Eligibility?</div>
</q-card-section>
<q-card-section v-if="equityToRevoke">
<p>Are you sure you want to revoke equity eligibility for this user?</p>
<p class="text-caption text-grey-7">
<strong>Note:</strong> This will prevent them from adding new expenses as equity contributions.
Their existing equity account and contributions will remain unchanged.
</p>
<q-list dense bordered class="rounded-borders q-mt-md">
<q-item>
<q-item-section>
<q-item-label caption>User</q-item-label>
<q-item-label>{% raw %}{{ equityToRevoke.user_id }}{% endraw %}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Equity Account</q-item-label>
<q-item-label>{% raw %}{{ equityToRevoke.equity_account_name }}{% endraw %}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="grey" v-close-popup></q-btn>
<q-btn
unelevated
label="Revoke"
color="negative"
@click="revokeEquityEligibility"
:loading="revokingEquity"
></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
{% endblock %}