Wire admin add-account endpoint into the UI #46

Merged
padreug merged 12 commits from feat/add-account-ui into main 2026-06-18 10:03:10 +00:00
2 changed files with 44 additions and 11 deletions
Showing only changes of commit 051c9f0c22 - Show all commits

feat(ui): constrain add-account to a root-type dropdown + sub-path

Free-typing the full hierarchical name let admins fat-finger the parent
(wrong/invalid root). Replace the single name field with a required
Account Type select (the 5 valid roots, mirroring _VALID_ACCOUNT_PREFIXES)
plus a sub-account input, a live 'Will create: ...' preview, and
per-segment validation (each part must be a capitalized Beancount
account component). The root prefix is now structurally guaranteed valid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Padreug 2026-06-16 01:15:06 +02:00

View file

@ -71,7 +71,8 @@ window.app = Vue.createApp({
},
addAccountDialog: {
show: false,
name: '',
rootType: 'Expenses',
subPath: '',
description: '',
loading: false
},
@ -292,6 +293,16 @@ window.app = Vue.createApp({
})
return options
},
accountRootTypes() {
// The five Beancount root account types — the only valid parents.
// Mirrors the server's _VALID_ACCOUNT_PREFIXES.
return ['Assets', 'Liabilities', 'Equity', 'Income', 'Expenses']
},
addAccountFullName() {
const sub = (this.addAccountDialog.subPath || '').trim().replace(/^:+|:+$/g, '')
if (!this.addAccountDialog.rootType || !sub) return ''
return `${this.addAccountDialog.rootType}:${sub}`
},
userOptions() {
const options = []
this.users.forEach(user => {
@ -573,17 +584,26 @@ window.app = Vue.createApp({
}
},
showAddAccountDialog() {
this.addAccountDialog.name = ''
this.addAccountDialog.rootType = 'Expenses'
this.addAccountDialog.subPath = ''
this.addAccountDialog.description = ''
this.addAccountDialog.show = true
},
async submitAddAccount() {
const name = (this.addAccountDialog.name || '').trim()
const validPrefixes = ['Assets:', 'Liabilities:', 'Equity:', 'Income:', 'Expenses:']
if (!validPrefixes.some(p => name.startsWith(p))) {
const name = this.addAccountFullName
if (!name) {
this.$q.notify({type: 'warning', message: 'Enter a sub-account name'})
return
}
// Each segment under the root must be a valid Beancount account
// component: start with an uppercase letter, then letters/digits/hyphens.
const badSegment = name.split(':').slice(1).find(
seg => !/^[A-Z][A-Za-z0-9-]*$/.test(seg)
)
if (badSegment !== undefined) {
this.$q.notify({
type: 'warning',
message: `Account name must start with one of: ${validPrefixes.join(', ')}`
message: `Invalid segment "${badSegment}" — each part must start with a capital letter (letters, digits, hyphens only)`
})
return
}

View file

@ -1251,15 +1251,28 @@
<q-form @submit="submitAddAccount" class="q-gutter-md">
<div class="text-h6 q-mb-md">Add Account</div>
<q-select
filled
dense
v-model="addAccountDialog.rootType"
:options="accountRootTypes"
label="Account Type *"
hint="Top-level category — the only valid parents"
></q-select>
<q-input
filled
dense
v-model.trim="addAccountDialog.name"
label="Account Name *"
placeholder="e.g., Expenses:Services:Domain"
hint="Full hierarchical name. Must start with Assets:, Liabilities:, Equity:, Income: or Expenses:"
v-model.trim="addAccountDialog.subPath"
label="Sub-account *"
placeholder="e.g., Vehicle:Gas"
hint="Path under the type. Use ':' to nest; capitalize each part."
></q-input>
<div v-if="addAccountFullName" class="text-caption text-grey">
Will create: <span class="text-weight-medium">{% raw %}{{ addAccountFullName }}{% endraw %}</span>
</div>
<q-input
filled
dense
@ -1279,7 +1292,7 @@
color="primary"
type="submit"
:loading="addAccountDialog.loading"
:disable="!addAccountDialog.name"
:disable="!addAccountFullName"
>
Create Account
</q-btn>