feat(libra/record): permission-gated Add Expense / Add Income buttons

Check the user's permitted accounts on mount; disable the card and
show a lock-icon caption directing them to contact an administrator
when they have no SUBMIT_EXPENSE / SUBMIT_INCOME access. Greys the
icon and badge background when disabled so the card reads inactive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-17 19:15:08 +02:00
commit 9e3de6ce16

View file

@ -1,21 +1,52 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useTimeAgo } from '@vueuse/core'
import { DollarSign, TrendingUp, FileText, Trash2, Image as ImageIcon } from 'lucide-vue-next'
import { DollarSign, TrendingUp, Lock, FileText, Trash2, Image as ImageIcon } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import AddExpense from '@/modules/expenses/components/AddExpense.vue'
import AddIncome from './AddIncome.vue'
import { useExpenseDrafts, type ExpenseDraft } from '../composables/useExpenseDrafts'
import { useAuth } from '@/composables/useAuthService'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { ExpensesAPI } from '@/modules/expenses/services/ExpensesAPI'
const { t } = useI18n()
const { drafts, hasDrafts, deleteDraft } = useExpenseDrafts()
const { user } = useAuth()
const expensesAPI = injectService<ExpensesAPI>(SERVICE_TOKENS.EXPENSES_API)
const showAddExpense = ref(false)
const showAddIncome = ref(false)
const permissionsLoaded = ref(false)
const canSubmitExpense = ref(true)
const canSubmitIncome = ref(true)
const expenseDisabled = computed(() => permissionsLoaded.value && !canSubmitExpense.value)
const incomeDisabled = computed(() => permissionsLoaded.value && !canSubmitIncome.value)
onMounted(async () => {
const walletKey = user.value?.wallets?.[0]?.inkey
if (!walletKey) return
try {
const accounts = await expensesAPI.getAccounts(walletKey, true, true)
canSubmitExpense.value = accounts.some(
(a) => a.name === 'Expenses' || a.name.startsWith('Expenses:')
)
canSubmitIncome.value = accounts.some(
(a) => a.name === 'Income' || a.name.startsWith('Income:')
)
} catch (error) {
console.error('[RecordPage] Failed to load user permissions:', error)
} finally {
permissionsLoaded.value = true
}
})
function handleExpenseSubmitted() {
// Could refresh balance or show notification
}
@ -50,32 +81,68 @@ function draftTimeAgo(isoDate: string) {
<!-- Action Cards -->
<div class="grid gap-4">
<!-- Add Expense Card -->
<button
class="flex items-start gap-4 p-5 rounded-xl border bg-card text-left transition-colors hover:bg-accent/50 active:bg-accent/70"
@click="showAddExpense = true"
>
<div class="flex items-center justify-center w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/20 shrink-0">
<DollarSign class="w-6 h-6 text-red-600 dark:text-red-400" />
<div>
<button
class="w-full flex items-start gap-4 p-5 rounded-xl border bg-card text-left transition-colors"
:class="expenseDisabled
? 'opacity-60 cursor-not-allowed'
: 'hover:bg-accent/50 active:bg-accent/70'"
:disabled="expenseDisabled"
@click="!expenseDisabled && (showAddExpense = true)"
>
<div
class="flex items-center justify-center w-12 h-12 rounded-full shrink-0"
:class="expenseDisabled ? 'bg-muted' : 'bg-red-100 dark:bg-red-900/20'"
>
<DollarSign
class="w-6 h-6"
:class="expenseDisabled ? 'text-muted-foreground' : 'text-red-600 dark:text-red-400'"
/>
</div>
<div class="min-w-0">
<h2 class="text-lg font-semibold text-foreground">{{ t('libra.record.addExpense') }}</h2>
<p class="text-sm text-muted-foreground mt-0.5">{{ t('libra.record.addExpenseDescription') }}</p>
</div>
</button>
<div v-if="expenseDisabled" class="mt-2 flex items-start gap-1.5 px-1">
<Lock class="w-3 h-3 mt-0.5 shrink-0 text-muted-foreground" />
<p class="text-[11px] leading-snug text-muted-foreground">
You don't have permission to submit expenses. Contact your administrator to request access.
</p>
</div>
<div class="min-w-0">
<h2 class="text-lg font-semibold text-foreground">{{ t('libra.record.addExpense') }}</h2>
<p class="text-sm text-muted-foreground mt-0.5">{{ t('libra.record.addExpenseDescription') }}</p>
</div>
</button>
</div>
<!-- Add Income Card -->
<button
class="flex items-start gap-4 p-5 rounded-xl border bg-card text-left transition-colors hover:bg-accent/50 active:bg-accent/70"
@click="showAddIncome = true"
>
<div class="flex items-center justify-center w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/20 shrink-0">
<TrendingUp class="w-6 h-6 text-green-600 dark:text-green-400" />
<div>
<button
class="w-full flex items-start gap-4 p-5 rounded-xl border bg-card text-left transition-colors"
:class="incomeDisabled
? 'opacity-60 cursor-not-allowed'
: 'hover:bg-accent/50 active:bg-accent/70'"
:disabled="incomeDisabled"
@click="!incomeDisabled && (showAddIncome = true)"
>
<div
class="flex items-center justify-center w-12 h-12 rounded-full shrink-0"
:class="incomeDisabled ? 'bg-muted' : 'bg-green-100 dark:bg-green-900/20'"
>
<TrendingUp
class="w-6 h-6"
:class="incomeDisabled ? 'text-muted-foreground' : 'text-green-600 dark:text-green-400'"
/>
</div>
<div class="min-w-0 flex-1">
<h2 class="text-lg font-semibold text-foreground">{{ t('libra.record.addIncome') }}</h2>
<p class="text-sm text-muted-foreground mt-0.5">{{ t('libra.record.addIncomeDescription') }}</p>
</div>
</button>
<div v-if="incomeDisabled" class="mt-2 flex items-start gap-1.5 px-1">
<Lock class="w-3 h-3 mt-0.5 shrink-0 text-muted-foreground" />
<p class="text-[11px] leading-snug text-muted-foreground">
You don't have permission to record income. Contact your administrator to request access.
</p>
</div>
<div class="min-w-0 flex-1">
<h2 class="text-lg font-semibold text-foreground">{{ t('libra.record.addIncome') }}</h2>
<p class="text-sm text-muted-foreground mt-0.5">{{ t('libra.record.addIncomeDescription') }}</p>
</div>
</button>
</div>
</div>
<!-- Drafts Section -->