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:
parent
33c294de7f
commit
eefabc3441
4 changed files with 338 additions and 1 deletions
|
|
@ -15,6 +15,7 @@ window.app = Vue.createApp({
|
||||||
users: [],
|
users: [],
|
||||||
settings: null,
|
settings: null,
|
||||||
userWalletSettings: null,
|
userWalletSettings: null,
|
||||||
|
userInfo: null, // User information including equity eligibility
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isSuperUser: false,
|
isSuperUser: false,
|
||||||
castleWalletConfigured: false,
|
castleWalletConfigured: false,
|
||||||
|
|
@ -353,6 +354,19 @@ window.app = Vue.createApp({
|
||||||
console.error('Error loading users:', error)
|
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() {
|
async loadSettings() {
|
||||||
try {
|
try {
|
||||||
// Try with admin key first to check settings
|
// 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
|
// Load settings first to determine if user is super user
|
||||||
await this.loadSettings()
|
await this.loadSettings()
|
||||||
await this.loadUserWallet()
|
await this.loadUserWallet()
|
||||||
|
await this.loadUserInfo()
|
||||||
await this.loadExchangeRate()
|
await this.loadExchangeRate()
|
||||||
await this.loadBalance()
|
await this.loadBalance()
|
||||||
await this.loadTransactions()
|
await this.loadTransactions()
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,19 @@ window.app = Vue.createApp({
|
||||||
accounts: [],
|
accounts: [],
|
||||||
users: [],
|
users: [],
|
||||||
filteredUsers: [],
|
filteredUsers: [],
|
||||||
|
equityEligibleUsers: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
granting: false,
|
granting: false,
|
||||||
revoking: false,
|
revoking: false,
|
||||||
|
grantingEquity: false,
|
||||||
|
revokingEquity: false,
|
||||||
activeTab: 'by-user',
|
activeTab: 'by-user',
|
||||||
showGrantDialog: false,
|
showGrantDialog: false,
|
||||||
showRevokeDialog: false,
|
showRevokeDialog: false,
|
||||||
|
showGrantEquityDialog: false,
|
||||||
|
showRevokeEquityDialog: false,
|
||||||
permissionToRevoke: null,
|
permissionToRevoke: null,
|
||||||
|
equityToRevoke: null,
|
||||||
isSuperUser: false,
|
isSuperUser: false,
|
||||||
grantForm: {
|
grantForm: {
|
||||||
user_id: '',
|
user_id: '',
|
||||||
|
|
@ -22,6 +28,10 @@ window.app = Vue.createApp({
|
||||||
notes: '',
|
notes: '',
|
||||||
expires_at: ''
|
expires_at: ''
|
||||||
},
|
},
|
||||||
|
grantEquityForm: {
|
||||||
|
user_id: '',
|
||||||
|
notes: ''
|
||||||
|
},
|
||||||
permissionTypeOptions: [
|
permissionTypeOptions: [
|
||||||
{
|
{
|
||||||
value: 'read',
|
value: 'read',
|
||||||
|
|
@ -326,6 +336,124 @@ window.app = Vue.createApp({
|
||||||
if (!dateString) return '-'
|
if (!dateString) return '-'
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
return date.toLocaleString()
|
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) {
|
if (this.isSuperUser) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.loadPermissions(),
|
this.loadPermissions(),
|
||||||
this.loadUsers()
|
this.loadUsers(),
|
||||||
|
this.loadEquityEligibleUsers()
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -723,6 +723,7 @@
|
||||||
></q-select>
|
></q-select>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
|
v-if="userInfo && userInfo.is_equity_eligible"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model="expenseDialog.isEquity"
|
v-model="expenseDialog.isEquity"
|
||||||
|
|
@ -735,8 +736,25 @@
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
label="Type *"
|
label="Type *"
|
||||||
|
hint="Choose whether this is a liability (Castle owes you) or an equity contribution"
|
||||||
></q-select>
|
></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
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
<q-tabs v-model="activeTab" class="text-primary" dense>
|
<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-user" icon="people" label="By User"></q-tab>
|
||||||
<q-tab name="by-account" icon="account_balance" label="By Account"></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-tabs>
|
||||||
|
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
|
@ -170,6 +171,68 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</q-tab-panel>
|
</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-tab-panels>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -348,4 +411,116 @@
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue