@mulmoclaude/accounting-plugin 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/atomic.d.ts +5 -3
- package/dist/server/atomic.d.ts.map +1 -1
- package/dist/server/context.d.ts +0 -7
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/http.d.ts +2 -2
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/io.d.ts.map +1 -1
- package/dist/server.cjs +35 -15
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +34 -14
- package/dist/server.js.map +1 -1
- package/dist/shared/api.d.ts +7 -0
- package/dist/shared/api.d.ts.map +1 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/paths.d.ts +7 -0
- package/dist/shared/paths.d.ts.map +1 -0
- package/dist/shared.cjs +16 -0
- package/dist/shared.cjs.map +1 -1
- package/dist/shared.js +15 -1
- package/dist/shared.js.map +1 -1
- package/dist/style.css +10 -10
- package/dist/vue/api.d.ts.map +1 -1
- package/dist/vue/hostContext.d.ts +8 -0
- package/dist/vue/hostContext.d.ts.map +1 -1
- package/dist/vue/index.d.ts +1 -1
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/lang/de.d.ts +4 -0
- package/dist/vue/lang/de.d.ts.map +1 -0
- package/dist/vue/lang/en.d.ts +220 -0
- package/dist/vue/lang/en.d.ts.map +1 -0
- package/dist/vue/lang/es.d.ts +4 -0
- package/dist/vue/lang/es.d.ts.map +1 -0
- package/dist/vue/lang/fr.d.ts +4 -0
- package/dist/vue/lang/fr.d.ts.map +1 -0
- package/dist/vue/lang/index.d.ts +233 -0
- package/dist/vue/lang/index.d.ts.map +1 -0
- package/dist/vue/lang/ja.d.ts +4 -0
- package/dist/vue/lang/ja.d.ts.map +1 -0
- package/dist/vue/lang/ko.d.ts +4 -0
- package/dist/vue/lang/ko.d.ts.map +1 -0
- package/dist/vue/lang/ptBR.d.ts +4 -0
- package/dist/vue/lang/ptBR.d.ts.map +1 -0
- package/dist/vue/lang/zh.d.ts +4 -0
- package/dist/vue/lang/zh.d.ts.map +1 -0
- package/dist/vue.cjs +1781 -21
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +1784 -24
- package/dist/vue.js.map +1 -1
- package/package.json +1 -1
package/dist/vue.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue.js","names":[],"sources":["../src/vue/hostContext.ts","../src/vue/api.ts","../src/vue/components/NewBookForm.vue","../src/vue/components/NewBookForm.vue","../src/vue/components/BookSwitcher.vue","../src/vue/components/BookSwitcher.vue","../src/vue/components/useLatestRequest.ts","../src/vue/components/DateRangePicker.vue","../src/vue/components/DateRangePicker.vue","../src/vue/components/accountNumbering.ts","../src/vue/components/AccountRow.vue","../src/vue/components/AccountRow.vue","../src/vue/components/accountValidation.ts","../src/vue/components/AccountEditor.vue","../src/vue/components/AccountEditor.vue","../src/vue/components/AccountsModal.vue","../src/vue/components/AccountsModal.vue","../src/vue/components/JournalEntryForm.vue","../src/vue/components/JournalEntryForm.vue","../src/vue/components/JournalList.vue","../src/vue/components/JournalList.vue","../src/vue/components/OpeningBalancesForm.vue","../src/vue/components/OpeningBalancesForm.vue","../src/vue/components/AccountsList.vue","../src/vue/components/AccountsList.vue","../src/vue/components/Ledger.vue","../src/vue/components/Ledger.vue","../src/vue/components/BalanceSheet.vue","../src/vue/components/BalanceSheet.vue","../src/vue/components/ProfitLoss.vue","../src/vue/components/ProfitLoss.vue","../src/vue/components/BookSettings.vue","../src/vue/components/BookSettings.vue","../src/vue/useAccountingChannel.ts","../src/vue/View.vue","../src/vue/View.vue","../src/vue/Preview.vue","../src/vue/Preview.vue"],"sourcesContent":["// Host-injected runtime context for the accounting Vue surface.\n//\n// The package can't reach into the host for its network client or its\n// raw pub/sub transport (that would be an uphill import, and would\n// hard-wire the package to MulmoClaude's internals). Instead the host\n// injects them once at startup via `configureAccountingHost(...)`, the\n// same module-level DI pattern `@mulmoclaude/collection-plugin` uses\n// (`configureCollectionUi`). MulmoTerminal wires its own equivalents.\n//\n// Two seams:\n// · apiCall — POST to /api/accounting; the host attaches the bearer\n// token + base URL. Returns the shared `ApiResult` union.\n// · subscribe — raw pub/sub channel subscription (socket.io in the\n// MulmoClaude host). The accounting backend publishes on\n// raw `accounting:<bookId>` channels, so the View needs\n// the raw transport, not the plugin-scoped pub/sub.\n\n/** Mirrors the host's `ApiResult<T>` (src/utils/api.ts) so callers\n * pattern-match on `.ok` without depending on the host module. */\nexport type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string; status: number };\n\n/** Network seam — structurally compatible with the host's `apiCall`.\n * `method` mirrors the host `ApiOptions[\"method\"]` union so the host\n * can pass its `apiCall` straight in without an adapter. */\nexport type AccountingApiCall = <T = unknown>(\n path: string,\n opts: { method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\"; body?: unknown },\n) => Promise<ApiResult<T>>;\n\n/** Pub/sub seam — structurally compatible with `usePubSub().subscribe`. */\nexport type AccountingSubscribe = (channel: string, handler: (payload: unknown) => void) => () => void;\n\nexport interface AccountingHostContext {\n apiCall: AccountingApiCall;\n subscribe: AccountingSubscribe;\n}\n\nlet ctx: AccountingHostContext | null = null;\n\n/** Called once by the host before any accounting View mounts. */\nexport function configureAccountingHost(context: AccountingHostContext): void {\n ctx = context;\n}\n\nfunction requireCtx(): AccountingHostContext {\n if (!ctx) {\n throw new Error(\"@mulmoclaude/accounting-plugin: configureAccountingHost() must be called before the accounting View mounts\");\n }\n return ctx;\n}\n\nexport function hostApiCall<T = unknown>(path: string, opts: { method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\"; body?: unknown }): Promise<ApiResult<T>> {\n return requireCtx().apiCall<T>(path, opts);\n}\n\nexport function hostSubscribe(channel: string, handler: (payload: unknown) => void): () => void {\n return requireCtx().subscribe(channel, handler);\n}\n","// Typed wrapper around POST /api/accounting. Centralises the action\n// names and the response shapes so the View / sub-components don't\n// repeat the cast at every call site.\n//\n// Every helper returns `ApiResult<T>` (the discriminated union mirrored\n// in hostContext.ts) — callers pattern-match on `.ok`. There is no\n// separate error-throwing path; all surfaces (network, HTTP, app\n// validation) flow through the same shape. The actual network client is\n// host-injected (see hostContext.ts) so the package stays host-agnostic.\n\nimport { hostApiCall as apiCall, type ApiResult } from \"./hostContext\";\nimport { ACCOUNTING_ACTIONS, type SupportedCountryCode, type FiscalYearEnd, type TimeSeriesGranularity, type TimeSeriesMetric } from \"../shared\";\n\nexport type AccountType = \"asset\" | \"liability\" | \"equity\" | \"income\" | \"expense\";\nexport type JournalEntryKind = \"normal\" | \"opening\" | \"void\" | \"void-marker\";\n\nexport interface Account {\n code: string;\n name: string;\n type: AccountType;\n note?: string;\n /** Soft-delete flag. When `false`, the account is hidden from\n * entry/ledger dropdowns but stays visible in Manage Accounts\n * and historical entries. */\n active?: boolean;\n}\n\nexport interface JournalLine {\n accountCode: string;\n debit?: number;\n credit?: number;\n memo?: string;\n /** Counterparty's tax-authority-issued registration ID — JP\n * T-number, EU VAT ID, UK VAT registration number, GSTIN, ABN,\n * etc. See server/accounting/types.ts for the full doc. */\n taxRegistrationId?: string;\n}\n\nexport interface JournalEntry {\n id: string;\n date: string;\n kind: JournalEntryKind;\n lines: JournalLine[];\n memo?: string;\n voidedEntryId?: string;\n voidReason?: string;\n /** Set on the new entry posted via the \"edit\" flow — id of the\n * original entry that was voided in the same operation. The\n * void + new-entry pair is two sequential calls on the client,\n * not an atomic transaction. */\n replacesEntryId?: string;\n createdAt: string;\n}\n\nexport interface BookSummary {\n id: string;\n name: string;\n currency: string;\n /** ISO 3166-1 alpha-2 country code identifying the tax jurisdiction\n * the book is kept under. Constrained to `SupportedCountryCode` —\n * see `countries.ts`. Optional for backward compatibility with\n * books created before the field was introduced. */\n country?: SupportedCountryCode;\n /** Which calendar-quarter end is the book's fiscal year end:\n * Q1 → March 31, Q2 → June 30, Q3 → September 30, Q4 → December 31.\n * Optional in the persisted shape for backward compatibility —\n * read-side code treats absence as Q4 via `resolveFiscalYearEnd`.\n * See `./fiscalYear.ts`. */\n fiscalYearEnd?: FiscalYearEnd;\n createdAt: string;\n}\n\nexport interface OpenAppPayload {\n kind: \"accounting-app\";\n /** `null` when the workspace has zero books — the View renders the\n * empty state and prompts for book creation. */\n bookId: string | null;\n initialTab?: string;\n}\n\nexport interface AccountBalance {\n accountCode: string;\n netDebit: number;\n}\n\nexport interface BalanceSheetSection {\n type: AccountType;\n rows: { accountCode: string; accountName: string; balance: number }[];\n total: number;\n}\n\nexport interface BalanceSheet {\n asOf: string;\n sections: BalanceSheetSection[];\n imbalance: number;\n}\n\nexport interface ProfitLoss {\n from: string;\n to: string;\n income: { rows: { accountCode: string; accountName: string; amount: number }[]; total: number };\n expense: { rows: { accountCode: string; accountName: string; amount: number }[]; total: number };\n netIncome: number;\n}\n\nexport interface LedgerRow {\n entryId: string;\n date: string;\n kind: JournalEntryKind;\n memo?: string;\n debit: number;\n credit: number;\n runningBalance: number;\n /** Counterparty tax-registration ID per source line. The Ledger\n * view shows it as its own column when the selected account is\n * in the input-tax band (14xx — see `isTaxAccountCode`). */\n taxRegistrationId?: string;\n}\n\nexport interface Ledger {\n accountCode: string;\n accountName: string;\n rows: LedgerRow[];\n closingBalance: number;\n}\n\nexport type ReportPeriod = { kind: \"month\"; period: string } | { kind: \"range\"; from: string; to: string };\n\n// The single dispatch route this plugin owns. The host META declares\n// the same `{ apiNamespace: \"accounting\", dispatch: { method: \"POST\",\n// path: \"\" } }`; we inline the resolved values here because the package\n// can't import the host-side META (it stays host-side for the\n// plugin-barrel codegen). The route is the stable contract between this\n// client and the package's own `./server` surface.\nconst DISPATCH_URL = \"/api/accounting\";\nconst DISPATCH_METHOD = \"POST\";\n\nfunction call<T>(action: string, args: Record<string, unknown> = {}): Promise<ApiResult<T>> {\n return apiCall<T>(DISPATCH_URL, { method: DISPATCH_METHOD, body: { action, ...args } });\n}\n\n// ── Books ────────────────────────────────────────────────────────────\n\nexport function getBooks(): Promise<ApiResult<{ books: BookSummary[] }>> {\n return call(ACCOUNTING_ACTIONS.getBooks);\n}\n\nexport function createBook(input: {\n name: string;\n currency?: string;\n country?: SupportedCountryCode;\n /** Q1..Q4 — required at the form boundary, but the server silently\n * defaults absent / empty to Q4 for back-compat. */\n fiscalYearEnd?: FiscalYearEnd;\n}): Promise<ApiResult<{ book: BookSummary }>> {\n return call(ACCOUNTING_ACTIONS.createBook, input);\n}\n\nexport function updateBook(input: {\n bookId: string;\n name?: string;\n /** Pass `\"\"` to explicitly clear the country (server treats it as\n * the \"drop the field\" sentinel). Any other value must be one of\n * the curated `SupportedCountryCode`s. */\n country?: SupportedCountryCode | \"\";\n /** Q1..Q4 — pure metadata, only changes how the date-range\n * shortcuts resolve. No \"clear\" path; absence leaves the existing\n * value untouched. */\n fiscalYearEnd?: FiscalYearEnd;\n}): Promise<ApiResult<{ book: BookSummary }>> {\n return call(ACCOUNTING_ACTIONS.updateBook, input);\n}\n\nexport function deleteBook(bookId: string): Promise<ApiResult<{ deletedBookId: string; deletedBookName: string }>> {\n return call(ACCOUNTING_ACTIONS.deleteBook, { bookId, confirm: true });\n}\n\n// ── Accounts ─────────────────────────────────────────────────────────\n\nexport function getAccounts(bookId: string): Promise<ApiResult<{ bookId: string; accounts: Account[] }>> {\n return call(ACCOUNTING_ACTIONS.getAccounts, { bookId });\n}\n\nexport function upsertAccount(account: Account, bookId: string): Promise<ApiResult<{ bookId: string; account: Account; accounts: Account[] }>> {\n return call(ACCOUNTING_ACTIONS.upsertAccount, { account, bookId });\n}\n\n// ── Entries ──────────────────────────────────────────────────────────\n\nexport interface AddEntriesItemInput {\n date: string;\n lines: JournalLine[];\n memo?: string;\n /** When set, marks this entry as the replacement posted via the\n * \"edit\" flow. The caller is expected to have voided\n * `replacesEntryId` separately just before this call — there is\n * no atomic transaction. */\n replacesEntryId?: string;\n}\n\nexport function addEntries(input: {\n bookId: string;\n /** One or more entries to post. The server validates every entry\n * before any write, so a single bad entry rejects the whole\n * batch. Pass a single-element array to post just one entry. */\n entries: AddEntriesItemInput[];\n}): Promise<ApiResult<{ bookId: string; entries: JournalEntry[] }>> {\n return call(ACCOUNTING_ACTIONS.addEntries, input);\n}\n\nexport function voidEntry(input: {\n entryId: string;\n reason?: string;\n bookId: string;\n}): Promise<ApiResult<{ bookId: string; reverseEntry: JournalEntry; markerEntry: JournalEntry }>> {\n return call(ACCOUNTING_ACTIONS.voidEntry, input);\n}\n\nexport function getJournalEntries(input: {\n from?: string;\n to?: string;\n accountCode?: string;\n bookId: string;\n}): Promise<ApiResult<{ bookId: string; entries: JournalEntry[]; voidedEntryIds: string[] }>> {\n return call(ACCOUNTING_ACTIONS.getJournalEntries, input);\n}\n\n// ── Opening balances ─────────────────────────────────────────────────\n\nexport function getOpeningBalances(bookId: string): Promise<ApiResult<{ bookId: string; opening: JournalEntry | null }>> {\n return call(ACCOUNTING_ACTIONS.getOpeningBalances, { bookId });\n}\n\nexport function setOpeningBalances(input: {\n asOfDate: string;\n lines: JournalLine[];\n memo?: string;\n bookId: string;\n}): Promise<ApiResult<{ bookId: string; openingEntry: JournalEntry; replacedExisting: boolean }>> {\n return call(ACCOUNTING_ACTIONS.setOpeningBalances, input);\n}\n\n// ── Reports ──────────────────────────────────────────────────────────\n\nexport function getBalanceSheet(period: ReportPeriod, bookId: string): Promise<ApiResult<{ bookId: string; balanceSheet: BalanceSheet }>> {\n return call(ACCOUNTING_ACTIONS.getReport, { kind: \"balance\", period, bookId });\n}\n\nexport function getProfitLoss(period: ReportPeriod, bookId: string): Promise<ApiResult<{ bookId: string; profitLoss: ProfitLoss }>> {\n return call(ACCOUNTING_ACTIONS.getReport, { kind: \"pl\", period, bookId });\n}\n\nexport function getLedger(accountCode: string, period: ReportPeriod | undefined, bookId: string): Promise<ApiResult<{ bookId: string; ledger: Ledger }>> {\n return call(ACCOUNTING_ACTIONS.getReport, { kind: \"ledger\", accountCode, period, bookId });\n}\n\nexport interface TimeSeriesPoint {\n label: string;\n from: string;\n to: string;\n value: number;\n}\n\nexport interface TimeSeriesInput {\n bookId: string;\n metric: TimeSeriesMetric;\n granularity: TimeSeriesGranularity;\n /** Inclusive YYYY-MM-DD lower bound. The first bucket is the one\n * CONTAINING this date — it can extend earlier. */\n from: string;\n /** Inclusive YYYY-MM-DD upper bound. The last bucket is the one\n * CONTAINING this date — it can extend later. */\n to: string;\n /** Required when metric === \"accountBalance\"; forbidden otherwise.\n * The server returns a 400 either way. */\n accountCode?: string;\n}\n\nexport interface TimeSeriesResult {\n bookId: string;\n metric: TimeSeriesMetric;\n granularity: TimeSeriesGranularity;\n from: string;\n to: string;\n accountCode?: string;\n points: TimeSeriesPoint[];\n}\n\nexport function getTimeSeries(input: TimeSeriesInput): Promise<ApiResult<TimeSeriesResult>> {\n // Spread so the named interface is widened into a fresh object\n // literal — `call()` takes `Record<string, unknown>` which a\n // declared interface doesn't satisfy structurally in TS.\n return call(ACCOUNTING_ACTIONS.getTimeSeries, { ...input });\n}\n\n// ── Admin ────────────────────────────────────────────────────────────\n\nexport function rebuildSnapshots(bookId: string): Promise<ApiResult<{ bookId: string; rebuilt: string[] }>> {\n return call(ACCOUNTING_ACTIONS.rebuildSnapshots, { bookId });\n}\n","<template>\n <!-- Form for creating a new book. Two layouts share one body:\n • modal (default) — used by BookSwitcher's \"+ New book…\"\n sentinel option. Backdrop click cancels.\n • fullPage — used by View.vue on the first-run flow when\n the workspace has zero books. No backdrop, no cancel:\n the user MUST create their first book to proceed.\n The submit calls createBook directly; on success it emits\n the new book and its id, leaving the parent to update its\n current selection / refetch. -->\n <div :class=\"wrapperClass\" data-testid=\"accounting-new-book-modal\" @click.self=\"onBackdropClick\">\n <form class=\"bg-white p-4 rounded shadow-lg w-96 flex flex-col gap-3\" data-testid=\"accounting-new-book-form\" @submit.prevent=\"onSubmit\">\n <h3 class=\"text-base font-semibold\">{{ t(\"pluginAccounting.bookSwitcher.newBook\") }}</h3>\n <p v-if=\"firstRun\" class=\"text-xs text-gray-500\" data-testid=\"accounting-new-book-firstrun\">{{ t(\"pluginAccounting.bookSwitcher.firstRunHint\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.nameLabel\") }}\n <input ref=\"nameInput\" v-model=\"name\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-new-book-name\" />\n </label>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.currencyLabel\") }}\n <select v-model=\"currency\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-new-book-currency\">\n <option v-for=\"opt in options\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.countryLabel\") }}\n <select v-model=\"country\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-new-book-country\">\n <option value=\"\">{{ t(\"pluginAccounting.bookSwitcher.countryPlaceholder\") }}</option>\n <option v-for=\"opt in countryOptions\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.countryHint\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndLabel\") }}\n <select\n v-model=\"fiscalYearEnd\"\n required\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-new-book-fiscal-year-end\"\n >\n <option v-for=\"opt in fiscalYearEndOptions\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndHint\") }}</p>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-new-book-error\">{{ error }}</p>\n <div class=\"flex justify-end gap-2 mt-1\">\n <button v-if=\"showCancel\" type=\"button\" class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-700 hover:bg-gray-50\" @click=\"onCancel\">\n {{ t(\"pluginAccounting.common.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-2.5 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm\"\n :disabled=\"creating\"\n data-testid=\"accounting-new-book-submit\"\n >\n {{ creating ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.bookSwitcher.create\") }}\n </button>\n </div>\n </form>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { createBook, type BookSummary } from \"../api\";\nimport {\n SUPPORTED_CURRENCY_CODES,\n localizedCurrencyName,\n SUPPORTED_COUNTRY_CODES,\n localizedCountryName,\n type SupportedCountryCode,\n DEFAULT_FISCAL_YEAR_END,\n FISCAL_YEAR_ENDS,\n type FiscalYearEnd,\n} from \"../../shared\";\n\nconst { t, locale } = useI18n();\n\nfunction regionFromLocaleTag(tag: string): SupportedCountryCode | \"\" {\n try {\n const { region } = new Intl.Locale(tag).maximize();\n if (region && (SUPPORTED_COUNTRY_CODES as readonly string[]).includes(region)) {\n return region as SupportedCountryCode;\n }\n } catch {\n /* fall through */\n }\n return \"\";\n}\n\nfunction guessDefaultCountry(uiLocaleTag: string): SupportedCountryCode | \"\" {\n // Try the active vue-i18n locale first (the user's *app* language\n // setting, e.g. \"ja-JP\"), then fall back to the browser's\n // navigator.language. Either may produce a region segment that maps\n // to a curated `SupportedCountryCode`. If neither does, leave the\n // field unset rather than silently picking a default — the\n // dropdown's \"(choose a country)\" option lets the user finish the\n // selection themselves so an unsupported locale doesn't quietly\n // become a US-jurisdiction book.\n const fromUi = regionFromLocaleTag(uiLocaleTag);\n if (fromUi !== \"\") return fromUi;\n const browserTag = typeof navigator !== \"undefined\" && typeof navigator.language === \"string\" ? navigator.language : \"\";\n return regionFromLocaleTag(browserTag);\n}\n\nconst props = withDefaults(\n defineProps<{\n firstRun?: boolean;\n cancelable?: boolean;\n fullPage?: boolean;\n }>(),\n { firstRun: false, cancelable: true, fullPage: false },\n);\n\nconst emit = defineEmits<{\n cancel: [];\n created: [book: BookSummary];\n}>();\n\nconst name = ref(\"\");\nconst currency = ref<string>(\"USD\");\nconst country = ref<SupportedCountryCode | \"\">(guessDefaultCountry(locale.value));\nconst fiscalYearEnd = ref<FiscalYearEnd>(DEFAULT_FISCAL_YEAR_END);\nconst creating = ref(false);\nconst error = ref<string | null>(null);\nconst nameInput = ref<HTMLInputElement | null>(null);\n\nonMounted(() => {\n // Land focus in Name on open — the only required field; the\n // currency select defaults to USD and the user usually leaves\n // it. Without this the user has to click into the field before\n // typing, which is friction for what should be a one-tap flow.\n void nextTick(() => nameInput.value?.focus());\n});\n\ninterface CurrencyOption {\n code: string;\n label: string;\n}\n\nconst options = computed<CurrencyOption[]>(() =>\n SUPPORTED_CURRENCY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCurrencyName(code, locale.value)}`,\n })),\n);\n\ninterface CountryOption {\n code: string;\n label: string;\n}\n\nconst countryOptions = computed<CountryOption[]>(() =>\n SUPPORTED_COUNTRY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCountryName(code, locale.value)}`,\n })),\n);\n\ninterface FiscalYearEndOption {\n value: FiscalYearEnd;\n label: string;\n}\n\nconst fiscalYearEndOptions = computed<FiscalYearEndOption[]>(() =>\n FISCAL_YEAR_ENDS.map((value) => ({\n value,\n label: t(`pluginAccounting.bookSwitcher.fiscalYearEnd${value}`),\n })),\n);\n\n// Full-page mode replaces the AccountingApp chrome — fill the\n// parent flex column with the form centered, no backdrop. Modal\n// mode keeps the original viewport overlay behaviour.\nconst wrapperClass = computed(() =>\n props.fullPage ? \"flex-1 bg-white flex items-center justify-center p-6 overflow-auto\" : \"fixed inset-0 z-50 bg-black/20 flex items-center justify-center\",\n);\n\n// Cancel is hidden in full-page mode regardless of `cancelable`\n// — the first-run flow forces the user to create a book.\nconst showCancel = computed(() => props.cancelable && !props.fullPage);\n\nfunction onBackdropClick(): void {\n if (props.fullPage) return;\n onCancel();\n}\n\nfunction onCancel(): void {\n if (!props.cancelable) return;\n emit(\"cancel\");\n}\n\nasync function onSubmit(): Promise<void> {\n if (creating.value) return;\n creating.value = true;\n error.value = null;\n try {\n // Only forward `country` when the user actually picked one — the\n // empty string is the dropdown's \"(choose a country)\" sentinel\n // and must not land on disk as a literal \"\" value.\n const pickedCountry: SupportedCountryCode | undefined = country.value === \"\" ? undefined : country.value;\n const result = await createBook({\n name: name.value.trim(),\n currency: currency.value,\n country: pickedCountry,\n fiscalYearEnd: fiscalYearEnd.value,\n });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n emit(\"created\", result.data.book);\n } finally {\n creating.value = false;\n }\n}\n</script>\n","<template>\n <!-- Form for creating a new book. Two layouts share one body:\n • modal (default) — used by BookSwitcher's \"+ New book…\"\n sentinel option. Backdrop click cancels.\n • fullPage — used by View.vue on the first-run flow when\n the workspace has zero books. No backdrop, no cancel:\n the user MUST create their first book to proceed.\n The submit calls createBook directly; on success it emits\n the new book and its id, leaving the parent to update its\n current selection / refetch. -->\n <div :class=\"wrapperClass\" data-testid=\"accounting-new-book-modal\" @click.self=\"onBackdropClick\">\n <form class=\"bg-white p-4 rounded shadow-lg w-96 flex flex-col gap-3\" data-testid=\"accounting-new-book-form\" @submit.prevent=\"onSubmit\">\n <h3 class=\"text-base font-semibold\">{{ t(\"pluginAccounting.bookSwitcher.newBook\") }}</h3>\n <p v-if=\"firstRun\" class=\"text-xs text-gray-500\" data-testid=\"accounting-new-book-firstrun\">{{ t(\"pluginAccounting.bookSwitcher.firstRunHint\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.nameLabel\") }}\n <input ref=\"nameInput\" v-model=\"name\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-new-book-name\" />\n </label>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.currencyLabel\") }}\n <select v-model=\"currency\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-new-book-currency\">\n <option v-for=\"opt in options\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.countryLabel\") }}\n <select v-model=\"country\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-new-book-country\">\n <option value=\"\">{{ t(\"pluginAccounting.bookSwitcher.countryPlaceholder\") }}</option>\n <option v-for=\"opt in countryOptions\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.countryHint\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndLabel\") }}\n <select\n v-model=\"fiscalYearEnd\"\n required\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-new-book-fiscal-year-end\"\n >\n <option v-for=\"opt in fiscalYearEndOptions\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndHint\") }}</p>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-new-book-error\">{{ error }}</p>\n <div class=\"flex justify-end gap-2 mt-1\">\n <button v-if=\"showCancel\" type=\"button\" class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-700 hover:bg-gray-50\" @click=\"onCancel\">\n {{ t(\"pluginAccounting.common.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-2.5 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm\"\n :disabled=\"creating\"\n data-testid=\"accounting-new-book-submit\"\n >\n {{ creating ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.bookSwitcher.create\") }}\n </button>\n </div>\n </form>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { createBook, type BookSummary } from \"../api\";\nimport {\n SUPPORTED_CURRENCY_CODES,\n localizedCurrencyName,\n SUPPORTED_COUNTRY_CODES,\n localizedCountryName,\n type SupportedCountryCode,\n DEFAULT_FISCAL_YEAR_END,\n FISCAL_YEAR_ENDS,\n type FiscalYearEnd,\n} from \"../../shared\";\n\nconst { t, locale } = useI18n();\n\nfunction regionFromLocaleTag(tag: string): SupportedCountryCode | \"\" {\n try {\n const { region } = new Intl.Locale(tag).maximize();\n if (region && (SUPPORTED_COUNTRY_CODES as readonly string[]).includes(region)) {\n return region as SupportedCountryCode;\n }\n } catch {\n /* fall through */\n }\n return \"\";\n}\n\nfunction guessDefaultCountry(uiLocaleTag: string): SupportedCountryCode | \"\" {\n // Try the active vue-i18n locale first (the user's *app* language\n // setting, e.g. \"ja-JP\"), then fall back to the browser's\n // navigator.language. Either may produce a region segment that maps\n // to a curated `SupportedCountryCode`. If neither does, leave the\n // field unset rather than silently picking a default — the\n // dropdown's \"(choose a country)\" option lets the user finish the\n // selection themselves so an unsupported locale doesn't quietly\n // become a US-jurisdiction book.\n const fromUi = regionFromLocaleTag(uiLocaleTag);\n if (fromUi !== \"\") return fromUi;\n const browserTag = typeof navigator !== \"undefined\" && typeof navigator.language === \"string\" ? navigator.language : \"\";\n return regionFromLocaleTag(browserTag);\n}\n\nconst props = withDefaults(\n defineProps<{\n firstRun?: boolean;\n cancelable?: boolean;\n fullPage?: boolean;\n }>(),\n { firstRun: false, cancelable: true, fullPage: false },\n);\n\nconst emit = defineEmits<{\n cancel: [];\n created: [book: BookSummary];\n}>();\n\nconst name = ref(\"\");\nconst currency = ref<string>(\"USD\");\nconst country = ref<SupportedCountryCode | \"\">(guessDefaultCountry(locale.value));\nconst fiscalYearEnd = ref<FiscalYearEnd>(DEFAULT_FISCAL_YEAR_END);\nconst creating = ref(false);\nconst error = ref<string | null>(null);\nconst nameInput = ref<HTMLInputElement | null>(null);\n\nonMounted(() => {\n // Land focus in Name on open — the only required field; the\n // currency select defaults to USD and the user usually leaves\n // it. Without this the user has to click into the field before\n // typing, which is friction for what should be a one-tap flow.\n void nextTick(() => nameInput.value?.focus());\n});\n\ninterface CurrencyOption {\n code: string;\n label: string;\n}\n\nconst options = computed<CurrencyOption[]>(() =>\n SUPPORTED_CURRENCY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCurrencyName(code, locale.value)}`,\n })),\n);\n\ninterface CountryOption {\n code: string;\n label: string;\n}\n\nconst countryOptions = computed<CountryOption[]>(() =>\n SUPPORTED_COUNTRY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCountryName(code, locale.value)}`,\n })),\n);\n\ninterface FiscalYearEndOption {\n value: FiscalYearEnd;\n label: string;\n}\n\nconst fiscalYearEndOptions = computed<FiscalYearEndOption[]>(() =>\n FISCAL_YEAR_ENDS.map((value) => ({\n value,\n label: t(`pluginAccounting.bookSwitcher.fiscalYearEnd${value}`),\n })),\n);\n\n// Full-page mode replaces the AccountingApp chrome — fill the\n// parent flex column with the form centered, no backdrop. Modal\n// mode keeps the original viewport overlay behaviour.\nconst wrapperClass = computed(() =>\n props.fullPage ? \"flex-1 bg-white flex items-center justify-center p-6 overflow-auto\" : \"fixed inset-0 z-50 bg-black/20 flex items-center justify-center\",\n);\n\n// Cancel is hidden in full-page mode regardless of `cancelable`\n// — the first-run flow forces the user to create a book.\nconst showCancel = computed(() => props.cancelable && !props.fullPage);\n\nfunction onBackdropClick(): void {\n if (props.fullPage) return;\n onCancel();\n}\n\nfunction onCancel(): void {\n if (!props.cancelable) return;\n emit(\"cancel\");\n}\n\nasync function onSubmit(): Promise<void> {\n if (creating.value) return;\n creating.value = true;\n error.value = null;\n try {\n // Only forward `country` when the user actually picked one — the\n // empty string is the dropdown's \"(choose a country)\" sentinel\n // and must not land on disk as a literal \"\" value.\n const pickedCountry: SupportedCountryCode | undefined = country.value === \"\" ? undefined : country.value;\n const result = await createBook({\n name: name.value.trim(),\n currency: currency.value,\n country: pickedCountry,\n fiscalYearEnd: fiscalYearEnd.value,\n });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n emit(\"created\", result.data.book);\n } finally {\n creating.value = false;\n }\n}\n</script>\n","<template>\n <div class=\"flex items-center gap-2\">\n <label class=\"text-xs text-gray-500\" for=\"accounting-book-select\">{{ t(\"pluginAccounting.bookSwitcher.label\") }}</label>\n <select\n id=\"accounting-book-select\"\n :value=\"modelValue\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-book-select\"\n @change=\"onSelect\"\n >\n <option v-if=\"modelValue === ''\" value=\"\" disabled>{{ t(\"pluginAccounting.bookSwitcher.placeholder\") }}</option>\n <option v-for=\"book in books\" :key=\"book.id\" :value=\"book.id\">{{ formatBookOption(book) }}</option>\n <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -- decorative separator inside the books <select>, not user copy -->\n <option disabled>──────────</option>\n <option :value=\"NEW_BOOK_SENTINEL\" data-testid=\"accounting-new-book-option\">+ {{ t(\"pluginAccounting.bookSwitcher.newBook\") }}</option>\n </select>\n <NewBookForm v-if=\"showNewBook\" @cancel=\"showNewBook = false\" @created=\"onCreated\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport NewBookForm from \"./NewBookForm.vue\";\nimport type { BookSummary } from \"../api\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ modelValue: string; books: BookSummary[] }>();\nconst emit = defineEmits<{\n \"update:modelValue\": [bookId: string];\n \"books-changed\": [];\n \"book-created\": [book: BookSummary];\n}>();\n\n// Sentinel value for the \"+ New book\" option living inside the\n// books <select>. Picking it opens the modal and reverts the\n// select's displayed value to the current selection — the option\n// must not collide with any real book id, which are nanoid-shaped.\nconst NEW_BOOK_SENTINEL = \"__new__\";\n\nconst showNewBook = ref(false);\n\nfunction formatBookOption(book: BookSummary): string {\n // `Name (CCY · Country)` when a country is set; otherwise fall back\n // to `Name (CCY)`. Keeps the option label compact while surfacing\n // the country so a multi-jurisdiction user can pick the right book\n // by tax regime, not just currency.\n const suffix = book.country ? `${book.currency} · ${book.country}` : book.currency;\n return `${book.name} (${suffix})`;\n}\n\nfunction onSelect(event: Event): void {\n const target = event.target as HTMLSelectElement;\n const bookId = target.value;\n if (bookId === NEW_BOOK_SENTINEL) {\n target.value = props.modelValue;\n showNewBook.value = true;\n return;\n }\n if (bookId === props.modelValue) return;\n // The View persists the new selection to localStorage; no server\n // round-trip needed since there's no shared \"active book\" state.\n emit(\"update:modelValue\", bookId);\n}\n\nfunction onCreated(book: BookSummary): void {\n // Hand the new book to the parent in one event so it can await\n // its own refetch before setting the active selection. Splitting\n // this into separate `books-changed` + `update:modelValue` emits\n // races: the parent's async refetch runs concurrently with the\n // selection update, and the stillExists guard inside refetch can\n // snap the selection back to books[0] if the fetch happens to\n // resolve before the new book is in the list.\n showNewBook.value = false;\n emit(\"book-created\", book);\n}\n</script>\n","<template>\n <div class=\"flex items-center gap-2\">\n <label class=\"text-xs text-gray-500\" for=\"accounting-book-select\">{{ t(\"pluginAccounting.bookSwitcher.label\") }}</label>\n <select\n id=\"accounting-book-select\"\n :value=\"modelValue\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-book-select\"\n @change=\"onSelect\"\n >\n <option v-if=\"modelValue === ''\" value=\"\" disabled>{{ t(\"pluginAccounting.bookSwitcher.placeholder\") }}</option>\n <option v-for=\"book in books\" :key=\"book.id\" :value=\"book.id\">{{ formatBookOption(book) }}</option>\n <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -- decorative separator inside the books <select>, not user copy -->\n <option disabled>──────────</option>\n <option :value=\"NEW_BOOK_SENTINEL\" data-testid=\"accounting-new-book-option\">+ {{ t(\"pluginAccounting.bookSwitcher.newBook\") }}</option>\n </select>\n <NewBookForm v-if=\"showNewBook\" @cancel=\"showNewBook = false\" @created=\"onCreated\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport NewBookForm from \"./NewBookForm.vue\";\nimport type { BookSummary } from \"../api\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ modelValue: string; books: BookSummary[] }>();\nconst emit = defineEmits<{\n \"update:modelValue\": [bookId: string];\n \"books-changed\": [];\n \"book-created\": [book: BookSummary];\n}>();\n\n// Sentinel value for the \"+ New book\" option living inside the\n// books <select>. Picking it opens the modal and reverts the\n// select's displayed value to the current selection — the option\n// must not collide with any real book id, which are nanoid-shaped.\nconst NEW_BOOK_SENTINEL = \"__new__\";\n\nconst showNewBook = ref(false);\n\nfunction formatBookOption(book: BookSummary): string {\n // `Name (CCY · Country)` when a country is set; otherwise fall back\n // to `Name (CCY)`. Keeps the option label compact while surfacing\n // the country so a multi-jurisdiction user can pick the right book\n // by tax regime, not just currency.\n const suffix = book.country ? `${book.currency} · ${book.country}` : book.currency;\n return `${book.name} (${suffix})`;\n}\n\nfunction onSelect(event: Event): void {\n const target = event.target as HTMLSelectElement;\n const bookId = target.value;\n if (bookId === NEW_BOOK_SENTINEL) {\n target.value = props.modelValue;\n showNewBook.value = true;\n return;\n }\n if (bookId === props.modelValue) return;\n // The View persists the new selection to localStorage; no server\n // round-trip needed since there's no shared \"active book\" state.\n emit(\"update:modelValue\", bookId);\n}\n\nfunction onCreated(book: BookSummary): void {\n // Hand the new book to the parent in one event so it can await\n // its own refetch before setting the active selection. Splitting\n // this into separate `books-changed` + `update:modelValue` emits\n // races: the parent's async refetch runs concurrently with the\n // selection update, and the stillExists guard inside refetch can\n // snap the selection back to books[0] if the fetch happens to\n // resolve before the new book is in the list.\n showNewBook.value = false;\n emit(\"book-created\", book);\n}\n</script>\n","// Stale-response guard for watcher-driven async fetches.\n//\n// Pattern: a watcher fires on bookId / filter / version changes\n// and kicks off `apiPost(...)`. Without coordination, a slower\n// earlier request can resolve after a newer one and overwrite the\n// fresh state with stale data. This composable hands out a\n// monotonic token before each await; the caller checks that the\n// token is still current after the await before mutating state.\n//\n// Usage:\n//\n// const { begin, isCurrent } = useLatestRequest();\n// async function refresh() {\n// const token = begin();\n// const result = await api.fetch(...);\n// if (!isCurrent(token)) return; // a newer refresh started\n// applyState(result);\n// }\n//\n// Cheap and dependency-free. Each component holds its own\n// `useLatestRequest()` instance — there's no shared state across\n// components.\n\nexport interface LatestRequestApi {\n /** Returns the token of the new request. Increments the\n * internal counter; older outstanding requests will fail\n * `isCurrent`. */\n begin: () => number;\n /** True if `token` is still the most recently issued one. */\n isCurrent: (token: number) => boolean;\n}\n\nexport function useLatestRequest(): LatestRequestApi {\n let counter = 0;\n return {\n begin(): number {\n counter += 1;\n return counter;\n },\n isCurrent(token: number): boolean {\n return token === counter;\n },\n };\n}\n","<template>\n <!-- Reusable from/to + shortcut date range picker. Owns no state\n beyond the v-model; the parent supplies an initial range and\n the active book's fiscalYearEnd so quarter/year shortcuts\n resolve under the right calendar. -->\n <div class=\"flex flex-wrap items-end gap-2\" data-testid=\"accounting-daterange\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.shortcutLabel\") }}\n <select\n :value=\"selectedShortcut\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-daterange-shortcut\"\n @change=\"onShortcutChange(($event.target as HTMLSelectElement).value)\"\n >\n <!-- Sentinel for the \"custom\" state. Hidden from the menu\n but bound when the current range doesn't match any\n preset, which leaves the trigger displaying blank\n instead of forcing a wrong-looking match. -->\n <option value=\"\" hidden></option>\n <option value=\"currentQuarter\">{{ t(\"pluginAccounting.dateRange.currentQuarter\") }}</option>\n <option value=\"previousQuarter\">{{ t(\"pluginAccounting.dateRange.previousQuarter\") }}</option>\n <option value=\"currentYear\">{{ t(\"pluginAccounting.dateRange.currentYear\") }}</option>\n <option value=\"previousYear\">{{ t(\"pluginAccounting.dateRange.previousYear\") }}</option>\n <option v-if=\"hasOpeningDate\" value=\"lifetime\">{{ t(\"pluginAccounting.dateRange.lifetime\") }}</option>\n <option value=\"all\">{{ t(\"pluginAccounting.dateRange.all\") }}</option>\n </select>\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.fromLabel\") }}\n <input\n :value=\"modelValue.from\"\n type=\"date\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm\"\n data-testid=\"accounting-daterange-from\"\n @input=\"onFromChange(($event.target as HTMLInputElement).value)\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.toLabel\") }}\n <input\n :value=\"modelValue.to\"\n type=\"date\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm\"\n data-testid=\"accounting-daterange-to\"\n @input=\"onToChange(($event.target as HTMLInputElement).value)\"\n />\n </label>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport {\n currentFiscalYearRange,\n currentQuarterRange,\n previousFiscalYearRange,\n previousQuarterRange,\n type DateRange,\n type FiscalYearEnd,\n localDateString,\n} from \"../../shared\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n modelValue: DateRange;\n fiscalYearEnd: FiscalYearEnd;\n /** The active book's opening-balance date. Drives the \"Lifetime\"\n * shortcut (from = openingDate, to = today). Optional — when\n * absent the Lifetime option is hidden from the menu. The opening\n * gate prevents the tabs that mount this picker from rendering\n * before an opening exists, so in normal use this stays defined. */\n openingDate?: string;\n}>();\n\nconst emit = defineEmits<{\n \"update:modelValue\": [DateRange];\n}>();\n\nconst hasOpeningDate = computed<boolean>(() => Boolean(props.openingDate));\n\nconst UNBOUNDED_RANGE: DateRange = { from: \"\", to: \"\" };\n\n/** From the book's opening date through today. Hidden from the menu\n * when the parent hasn't supplied an opening. */\nfunction lifetimeRange(): DateRange | null {\n if (!props.openingDate) return null;\n return { from: props.openingDate, to: localDateString() };\n}\n\ntype Shortcut = \"currentQuarter\" | \"previousQuarter\" | \"currentYear\" | \"previousYear\" | \"lifetime\" | \"all\";\n/** Empty string is the sentinel \"no preset matches\" value bound to\n * the hidden option in the template — the trigger shows blank. */\ntype SelectedShortcut = Shortcut | \"\";\n\nfunction rangesEqual(left: DateRange, right: DateRange): boolean {\n return left.from === right.from && left.to === right.to;\n}\n\n// Resolve the dropdown's displayed value from the current\n// modelValue. Re-evaluates today on every read — the picker is a\n// short-lived UI surface so cache invalidation isn't a concern, and\n// the user has no expectation that \"current quarter\" picked in the\n// morning still labels correctly at midnight. Returns \"\" when no\n// preset matches (custom range from manual from/to edits).\n//\n// Order matters when ranges collide: when no opening is on file the\n// Lifetime option is hidden from the menu, but if it ever produced\n// the same span as another preset (it can't — Lifetime spans years,\n// presets span quarter/year), the earlier branch would win. We\n// check the explicit ranges first and fall through to the unbounded\n// \"all\" last so a manually-cleared input lands on \"all\" rather than\n// blank when both sides happen to be empty.\nconst selectedShortcut = computed<SelectedShortcut>(() => {\n const value = props.modelValue;\n const today = new Date();\n if (rangesEqual(value, currentQuarterRange(props.fiscalYearEnd, today))) return \"currentQuarter\";\n if (rangesEqual(value, previousQuarterRange(props.fiscalYearEnd, today))) return \"previousQuarter\";\n if (rangesEqual(value, currentFiscalYearRange(props.fiscalYearEnd, today))) return \"currentYear\";\n if (rangesEqual(value, previousFiscalYearRange(props.fiscalYearEnd, today))) return \"previousYear\";\n const lifetime = lifetimeRange();\n if (lifetime && rangesEqual(value, lifetime)) return \"lifetime\";\n if (rangesEqual(value, UNBOUNDED_RANGE)) return \"all\";\n return \"\";\n});\n\nfunction onShortcutChange(raw: string): void {\n const today = new Date();\n if (raw === \"currentQuarter\") emit(\"update:modelValue\", currentQuarterRange(props.fiscalYearEnd, today));\n else if (raw === \"previousQuarter\") emit(\"update:modelValue\", previousQuarterRange(props.fiscalYearEnd, today));\n else if (raw === \"currentYear\") emit(\"update:modelValue\", currentFiscalYearRange(props.fiscalYearEnd, today));\n else if (raw === \"previousYear\") emit(\"update:modelValue\", previousFiscalYearRange(props.fiscalYearEnd, today));\n else if (raw === \"lifetime\") {\n const lifetime = lifetimeRange();\n if (lifetime) emit(\"update:modelValue\", lifetime);\n } else if (raw === \"all\") emit(\"update:modelValue\", UNBOUNDED_RANGE);\n}\n\nfunction onFromChange(value: string): void {\n emit(\"update:modelValue\", { from: value, to: props.modelValue.to });\n}\n\nfunction onToChange(value: string): void {\n emit(\"update:modelValue\", { from: props.modelValue.from, to: value });\n}\n</script>\n","<template>\n <!-- Reusable from/to + shortcut date range picker. Owns no state\n beyond the v-model; the parent supplies an initial range and\n the active book's fiscalYearEnd so quarter/year shortcuts\n resolve under the right calendar. -->\n <div class=\"flex flex-wrap items-end gap-2\" data-testid=\"accounting-daterange\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.shortcutLabel\") }}\n <select\n :value=\"selectedShortcut\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-daterange-shortcut\"\n @change=\"onShortcutChange(($event.target as HTMLSelectElement).value)\"\n >\n <!-- Sentinel for the \"custom\" state. Hidden from the menu\n but bound when the current range doesn't match any\n preset, which leaves the trigger displaying blank\n instead of forcing a wrong-looking match. -->\n <option value=\"\" hidden></option>\n <option value=\"currentQuarter\">{{ t(\"pluginAccounting.dateRange.currentQuarter\") }}</option>\n <option value=\"previousQuarter\">{{ t(\"pluginAccounting.dateRange.previousQuarter\") }}</option>\n <option value=\"currentYear\">{{ t(\"pluginAccounting.dateRange.currentYear\") }}</option>\n <option value=\"previousYear\">{{ t(\"pluginAccounting.dateRange.previousYear\") }}</option>\n <option v-if=\"hasOpeningDate\" value=\"lifetime\">{{ t(\"pluginAccounting.dateRange.lifetime\") }}</option>\n <option value=\"all\">{{ t(\"pluginAccounting.dateRange.all\") }}</option>\n </select>\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.fromLabel\") }}\n <input\n :value=\"modelValue.from\"\n type=\"date\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm\"\n data-testid=\"accounting-daterange-from\"\n @input=\"onFromChange(($event.target as HTMLInputElement).value)\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.toLabel\") }}\n <input\n :value=\"modelValue.to\"\n type=\"date\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm\"\n data-testid=\"accounting-daterange-to\"\n @input=\"onToChange(($event.target as HTMLInputElement).value)\"\n />\n </label>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport {\n currentFiscalYearRange,\n currentQuarterRange,\n previousFiscalYearRange,\n previousQuarterRange,\n type DateRange,\n type FiscalYearEnd,\n localDateString,\n} from \"../../shared\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n modelValue: DateRange;\n fiscalYearEnd: FiscalYearEnd;\n /** The active book's opening-balance date. Drives the \"Lifetime\"\n * shortcut (from = openingDate, to = today). Optional — when\n * absent the Lifetime option is hidden from the menu. The opening\n * gate prevents the tabs that mount this picker from rendering\n * before an opening exists, so in normal use this stays defined. */\n openingDate?: string;\n}>();\n\nconst emit = defineEmits<{\n \"update:modelValue\": [DateRange];\n}>();\n\nconst hasOpeningDate = computed<boolean>(() => Boolean(props.openingDate));\n\nconst UNBOUNDED_RANGE: DateRange = { from: \"\", to: \"\" };\n\n/** From the book's opening date through today. Hidden from the menu\n * when the parent hasn't supplied an opening. */\nfunction lifetimeRange(): DateRange | null {\n if (!props.openingDate) return null;\n return { from: props.openingDate, to: localDateString() };\n}\n\ntype Shortcut = \"currentQuarter\" | \"previousQuarter\" | \"currentYear\" | \"previousYear\" | \"lifetime\" | \"all\";\n/** Empty string is the sentinel \"no preset matches\" value bound to\n * the hidden option in the template — the trigger shows blank. */\ntype SelectedShortcut = Shortcut | \"\";\n\nfunction rangesEqual(left: DateRange, right: DateRange): boolean {\n return left.from === right.from && left.to === right.to;\n}\n\n// Resolve the dropdown's displayed value from the current\n// modelValue. Re-evaluates today on every read — the picker is a\n// short-lived UI surface so cache invalidation isn't a concern, and\n// the user has no expectation that \"current quarter\" picked in the\n// morning still labels correctly at midnight. Returns \"\" when no\n// preset matches (custom range from manual from/to edits).\n//\n// Order matters when ranges collide: when no opening is on file the\n// Lifetime option is hidden from the menu, but if it ever produced\n// the same span as another preset (it can't — Lifetime spans years,\n// presets span quarter/year), the earlier branch would win. We\n// check the explicit ranges first and fall through to the unbounded\n// \"all\" last so a manually-cleared input lands on \"all\" rather than\n// blank when both sides happen to be empty.\nconst selectedShortcut = computed<SelectedShortcut>(() => {\n const value = props.modelValue;\n const today = new Date();\n if (rangesEqual(value, currentQuarterRange(props.fiscalYearEnd, today))) return \"currentQuarter\";\n if (rangesEqual(value, previousQuarterRange(props.fiscalYearEnd, today))) return \"previousQuarter\";\n if (rangesEqual(value, currentFiscalYearRange(props.fiscalYearEnd, today))) return \"currentYear\";\n if (rangesEqual(value, previousFiscalYearRange(props.fiscalYearEnd, today))) return \"previousYear\";\n const lifetime = lifetimeRange();\n if (lifetime && rangesEqual(value, lifetime)) return \"lifetime\";\n if (rangesEqual(value, UNBOUNDED_RANGE)) return \"all\";\n return \"\";\n});\n\nfunction onShortcutChange(raw: string): void {\n const today = new Date();\n if (raw === \"currentQuarter\") emit(\"update:modelValue\", currentQuarterRange(props.fiscalYearEnd, today));\n else if (raw === \"previousQuarter\") emit(\"update:modelValue\", previousQuarterRange(props.fiscalYearEnd, today));\n else if (raw === \"currentYear\") emit(\"update:modelValue\", currentFiscalYearRange(props.fiscalYearEnd, today));\n else if (raw === \"previousYear\") emit(\"update:modelValue\", previousFiscalYearRange(props.fiscalYearEnd, today));\n else if (raw === \"lifetime\") {\n const lifetime = lifetimeRange();\n if (lifetime) emit(\"update:modelValue\", lifetime);\n } else if (raw === \"all\") emit(\"update:modelValue\", UNBOUNDED_RANGE);\n}\n\nfunction onFromChange(value: string): void {\n emit(\"update:modelValue\", { from: value, to: props.modelValue.to });\n}\n\nfunction onToChange(value: string): void {\n emit(\"update:modelValue\", { from: props.modelValue.from, to: value });\n}\n</script>\n","// Account-code numbering convention. The chart of accounts uses\n// 4-digit codes whose leading digit identifies the type:\n//\n// 1xxx → asset\n// 2xxx → liability\n// 3xxx → equity\n// 4xxx → income\n// 5xxx → expense\n//\n// Within those bands, the second digit `4` is reserved for tax-\n// related accounts on both sides of the balance sheet:\n//\n// 14xx → tax-related current assets\n// (1400 Input Tax Receivable / 仮払消費税, plus future\n// withholding-tax-receivable / etc. siblings)\n// 24xx → tax-related current liabilities\n// (2400 Sales Tax Payable / 仮受消費税, plus future\n// withholding-tax-payable / etc. siblings)\n//\n// Special-case UI (Ledger T-number column, JournalEntryForm\n// per-line tax-registration ID input) is **input-tax-only** — it\n// keys off `isTaxAccountCode`, which matches 14xx (purchase side)\n// only. Output-tax / sales-side lines (24xx) intentionally don't\n// surface a counterparty registration field: the seller's\n// obligation is to put their *own* registration number on the\n// invoice they issue, not to capture the customer's. So a custom\n// suspense account added in the 14xx band participates without\n// any opt-in step; 24xx accounts book the liability without the\n// extra column.\n//\n// Lives in its own module so AccountsModal, AccountEditor, and the\n// validation helper can share the same constants without circular\n// imports between Vue components.\n\nimport type { Account, AccountType } from \"../api\";\n\nexport const ACCOUNT_TYPE_PREFIX: Record<AccountType, number> = {\n asset: 1,\n liability: 2,\n equity: 3,\n income: 4,\n expense: 5,\n};\n\nconst TAX_ACCOUNT_PREFIXES: readonly string[] = [\"14\"];\n\n/** Returns `true` for codes whose first two digits identify a\n * tax-related current asset (`14xx`) — i.e. the input-tax /\n * purchase side of consumption / sales / VAT bookkeeping. Drives\n * Ledger column visibility and the JournalEntryForm per-line\n * tax-registration ID input. Output-tax (24xx) is intentionally\n * excluded: the counterparty's registration ID is only\n * load-bearing for input-tax-credit eligibility on purchases. */\nexport function isTaxAccountCode(code: string): boolean {\n return TAX_ACCOUNT_PREFIXES.some((prefix) => code.startsWith(prefix));\n}\n\nconst ACCOUNT_CODE_RE = /^\\d{4}$/;\nconst SUGGESTED_GAP = 10;\n\nexport function isValidAccountCode(code: string): boolean {\n return ACCOUNT_CODE_RE.test(code);\n}\n\nexport function typeForCode(code: string): AccountType | null {\n if (!isValidAccountCode(code)) return null;\n const leading = Number.parseInt(code[0], 10);\n for (const [type, prefix] of Object.entries(ACCOUNT_TYPE_PREFIX) as [AccountType, number][]) {\n if (prefix === leading) return type;\n }\n return null;\n}\n\nexport function codeMatchesType(code: string, type: AccountType): boolean {\n return typeForCode(code) === type;\n}\n\n/** Suggest the next free 4-digit code for `type`. Picks max-in-range\n * + SUGGESTED_GAP so users keep room to insert sibling accounts\n * later (the standard accounting convention). Falls back to the\n * prefix base when the range is empty, and to max+1 when +gap would\n * spill out of the 4-digit prefix window. */\nexport function suggestNextCode(type: AccountType, accounts: readonly Account[]): string {\n const prefix = ACCOUNT_TYPE_PREFIX[type];\n const inRange: number[] = [];\n for (const account of accounts) {\n if (!isValidAccountCode(account.code)) continue;\n const value = Number.parseInt(account.code, 10);\n if (Math.floor(value / 1000) !== prefix) continue;\n inRange.push(value);\n }\n if (inRange.length === 0) return `${prefix}000`;\n const max = Math.max(...inRange);\n const candidate = max + SUGGESTED_GAP;\n if (Math.floor(candidate / 1000) === prefix && candidate <= 9999) return String(candidate);\n // Range is dense at the top — fall back to a unit step. If even\n // that overflows the prefix window the chart is essentially full\n // for that type; surface the overflow rather than silently\n // suggesting a code in the next type's range.\n const fallback = max + 1;\n if (Math.floor(fallback / 1000) === prefix && fallback <= 9999) return String(fallback);\n return `${prefix}999`;\n}\n","<template>\n <!-- One row in the AccountsModal list. Read-only display + an\n active checkbox (left column) and an Edit button (right) for\n active rows. The editor itself is AccountEditor.vue, mounted\n in place of this row by the parent when editing. -->\n <div :class=\"['flex items-center gap-2 px-2 py-0.5 text-sm', inactive ? 'opacity-60' : '']\" :data-testid=\"`accounting-accounts-row-${account.code}`\">\n <input\n type=\"checkbox\"\n :checked=\"!inactive\"\n :title=\"inactive ? t('pluginAccounting.accounts.reactivate') : t('pluginAccounting.accounts.deactivate')\"\n :aria-label=\"inactive ? t('pluginAccounting.accounts.reactivate') : t('pluginAccounting.accounts.deactivate')\"\n class=\"h-4 w-4 shrink-0 cursor-pointer\"\n :data-testid=\"`accounting-accounts-toggle-${account.code}`\"\n @change=\"emit('toggleActive')\"\n />\n <span class=\"font-mono text-xs text-gray-500 w-16 shrink-0\">{{ account.code }}</span>\n <span\n :class=\"['grow min-w-0 truncate', inactive ? 'line-through' : '']\"\n :data-testid=\"inactive ? `accounting-accounts-inactive-${account.code}` : undefined\"\n >{{ account.name }}</span\n >\n <span v-if=\"account.note\" class=\"text-xs text-gray-400 truncate max-w-[8rem]\" :title=\"account.note\">{{ account.note }}</span>\n <!-- Always rendered (with `invisible` when inactive) so checking\n and unchecking the active box doesn't shift the row width. -->\n <button\n type=\"button\"\n :class=\"['h-8 px-2.5 rounded text-sm text-blue-600 hover:bg-blue-50', inactive ? 'invisible' : '']\"\n :data-testid=\"`accounting-accounts-edit-${account.code}`\"\n :disabled=\"inactive\"\n :aria-hidden=\"inactive ? 'true' : undefined\"\n :tabindex=\"inactive ? -1 : undefined\"\n @click=\"emit('edit')\"\n >\n {{ t(\"pluginAccounting.accounts.edit\") }}\n </button>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type { Account } from \"../api\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ account: Account }>();\nconst emit = defineEmits<{ edit: []; toggleActive: [] }>();\n\nconst inactive = computed(() => props.account.active === false);\n</script>\n","<template>\n <!-- One row in the AccountsModal list. Read-only display + an\n active checkbox (left column) and an Edit button (right) for\n active rows. The editor itself is AccountEditor.vue, mounted\n in place of this row by the parent when editing. -->\n <div :class=\"['flex items-center gap-2 px-2 py-0.5 text-sm', inactive ? 'opacity-60' : '']\" :data-testid=\"`accounting-accounts-row-${account.code}`\">\n <input\n type=\"checkbox\"\n :checked=\"!inactive\"\n :title=\"inactive ? t('pluginAccounting.accounts.reactivate') : t('pluginAccounting.accounts.deactivate')\"\n :aria-label=\"inactive ? t('pluginAccounting.accounts.reactivate') : t('pluginAccounting.accounts.deactivate')\"\n class=\"h-4 w-4 shrink-0 cursor-pointer\"\n :data-testid=\"`accounting-accounts-toggle-${account.code}`\"\n @change=\"emit('toggleActive')\"\n />\n <span class=\"font-mono text-xs text-gray-500 w-16 shrink-0\">{{ account.code }}</span>\n <span\n :class=\"['grow min-w-0 truncate', inactive ? 'line-through' : '']\"\n :data-testid=\"inactive ? `accounting-accounts-inactive-${account.code}` : undefined\"\n >{{ account.name }}</span\n >\n <span v-if=\"account.note\" class=\"text-xs text-gray-400 truncate max-w-[8rem]\" :title=\"account.note\">{{ account.note }}</span>\n <!-- Always rendered (with `invisible` when inactive) so checking\n and unchecking the active box doesn't shift the row width. -->\n <button\n type=\"button\"\n :class=\"['h-8 px-2.5 rounded text-sm text-blue-600 hover:bg-blue-50', inactive ? 'invisible' : '']\"\n :data-testid=\"`accounting-accounts-edit-${account.code}`\"\n :disabled=\"inactive\"\n :aria-hidden=\"inactive ? 'true' : undefined\"\n :tabindex=\"inactive ? -1 : undefined\"\n @click=\"emit('edit')\"\n >\n {{ t(\"pluginAccounting.accounts.edit\") }}\n </button>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type { Account } from \"../api\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ account: Account }>();\nconst emit = defineEmits<{ edit: []; toggleActive: [] }>();\n\nconst inactive = computed(() => props.account.active === false);\n</script>\n","// Pure validation for the AccountsModal editor draft. Lives in its\n// own module so unit tests can exercise the boundary cases (reserved\n// `_` prefix, duplicate code, empty fields) without spinning up Vue\n// or i18n. The component maps the returned error code to a\n// localized message.\n//\n// The `_`-prefix rule mirrors the server's check in\n// server/accounting/service.ts:upsertAccount — codes starting with\n// `_` are reserved for synthetic report rows. Catching it client-\n// side avoids a round-trip and surfaces the localized message\n// instead of the raw server error.\n\nimport type { Account } from \"../api\";\nimport type { AccountDraft } from \"./accountDraft\";\nimport { codeMatchesType, isValidAccountCode } from \"./accountNumbering\";\n\nexport const RESERVED_PREFIX = \"_\";\n\nexport type CodeValidationError = \"emptyCode\" | \"reservedCode\" | \"invalidCodeFormat\" | \"codeTypeMismatch\" | \"duplicateCode\";\nexport type NameValidationError = \"emptyName\" | \"duplicateName\";\nexport type AccountValidationError = CodeValidationError | NameValidationError;\n\n/**\n * Validate just the code field. Split out from the full draft\n * validator so AccountEditor can paint a per-field red border in\n * realtime without re-running the name check on every keystroke.\n */\nexport function validateCodeField(draft: AccountDraft, existing: readonly Account[], isNew: boolean): CodeValidationError | null {\n const trimmedCode = draft.code.trim();\n if (trimmedCode.length === 0) return \"emptyCode\";\n if (trimmedCode.startsWith(RESERVED_PREFIX)) return \"reservedCode\";\n // 4-digit numbering is enforced for new accounts only: pre-existing\n // books may already hold legacy codes the user added before the\n // rule landed, and changing the code would orphan their journal\n // lines (codes are immutable once created — see codeReadOnlyHint).\n if (isNew && !isValidAccountCode(trimmedCode)) return \"invalidCodeFormat\";\n if (isNew && !codeMatchesType(trimmedCode, draft.type)) return \"codeTypeMismatch\";\n if (isNew && existing.some((account) => account.code === trimmedCode)) return \"duplicateCode\";\n return null;\n}\n\n/**\n * Validate just the name field. Empty + duplicate (case-insensitive,\n * trimmed) against other accounts. On edit, the account being edited\n * is excluded from the duplicate check via `draft.code` — otherwise\n * every save would flag the user's own row as a collision.\n */\nexport function validateNameField(draft: AccountDraft, existing: readonly Account[], isNew: boolean): NameValidationError | null {\n const trimmedName = draft.name.trim();\n if (trimmedName.length === 0) return \"emptyName\";\n const folded = trimmedName.toLowerCase();\n const collides = existing.some((account) => {\n if (!isNew && account.code === draft.code.trim()) return false;\n return account.name.trim().toLowerCase() === folded;\n });\n if (collides) return \"duplicateName\";\n return null;\n}\n\n/**\n * Validate a draft about to be sent to `upsertAccount`. Returns\n * `null` on success or an error code on failure. Caller maps the\n * code to a localized message.\n *\n * `existing` is the current chart of accounts — used to detect a\n * duplicate code on a brand-new entry (otherwise the server would\n * silently overwrite the existing account, which is rarely what\n * the user typing into the \"Add account\" form intended).\n *\n * Code errors take precedence over name errors so the user fixes\n * one stable issue at a time as they type.\n */\nexport function validateAccountDraft(draft: AccountDraft, existing: readonly Account[], isNew: boolean): AccountValidationError | null {\n return validateCodeField(draft, existing, isNew) ?? validateNameField(draft, existing, isNew);\n}\n","<template>\n <!-- Inline editor used by AccountsModal both for \"Edit\" on an\n existing row and per-section \"+ Add\" buttons. The parent\n owns the open/closed state and the draft instance — this\n component is dumb, but it runs realtime per-field validation\n (red border) so the user gets feedback before clicking Save. -->\n <form\n class=\"flex flex-col gap-2 p-2 border border-blue-200 bg-blue-50/40 rounded text-sm\"\n :data-testid=\"isNew ? 'accounting-accounts-form-new' : `accounting-accounts-form-edit-${draft.code}`\"\n @submit.prevent=\"onSubmit\"\n >\n <div class=\"flex flex-wrap gap-2\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-28\">\n {{ t(\"pluginAccounting.accounts.columnCode\") }}\n <!-- New accounts: leading digit is fixed by type, so the\n editable input is restricted to the trailing 3 digits.\n The prefix span communicates the rule visually without\n needing a separate help string. -->\n <!-- The trailing-3-digit input has `outline-none bg-transparent`\n so the prefix span and the editable digits read as one\n pill. That removes the browser's default focus indicator,\n so we surface it on the wrapper via `focus-within:ring-1`\n — same shape as the name input below — to keep the field\n keyboard-discoverable (#1115 review). -->\n <div\n v-if=\"isNew\"\n :class=\"[\n 'flex items-stretch h-8 rounded border bg-white text-sm font-mono overflow-hidden',\n codeError ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-300 focus-within:ring-1 focus-within:ring-blue-500',\n ]\"\n >\n <span\n class=\"px-2 flex items-center bg-gray-100 text-gray-500 border-r border-gray-200 select-none\"\n data-testid=\"accounting-accounts-form-code-prefix\"\n >{{ codePrefix }}</span\n >\n <input\n v-model=\"codeTrailing\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"3\"\n pattern=\"\\d{3}\"\n class=\"px-2 grow w-0 outline-none bg-transparent\"\n data-testid=\"accounting-accounts-form-code\"\n @input=\"codeTouched = true\"\n />\n </div>\n <!-- Edit: code is immutable, so we display the actual stored\n value as a single disabled field rather than splitting\n prefix + trailing (legacy non-4-digit codes would\n otherwise be misrendered). -->\n <input\n v-else\n v-model=\"local.code\"\n type=\"text\"\n disabled\n class=\"h-8 px-2 rounded border border-gray-300 text-sm font-mono bg-gray-100 text-gray-500\"\n data-testid=\"accounting-accounts-form-code\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 grow min-w-[10rem]\">\n {{ t(\"pluginAccounting.accounts.columnName\") }}\n <input\n ref=\"nameInput\"\n v-model=\"local.name\"\n type=\"text\"\n :class=\"[\n 'h-8 px-2 rounded border text-sm focus:outline-none',\n nameError ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-300 focus:ring-1 focus:ring-blue-500',\n ]\"\n data-testid=\"accounting-accounts-form-name\"\n @input=\"nameTouched = true\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-32\">\n {{ t(\"pluginAccounting.accounts.columnType\") }}\n <!-- Type is locked in both modes:\n - NEW: the per-category \"+ Add\" button already chose\n it, and the suggested code is keyed off it.\n - EDIT: the type is part of the account's identity (it\n drives section placement, the code-prefix rule, and\n report categorization); changing it after the fact\n leads to surprising downstream effects. -->\n <select\n v-model=\"local.type\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white disabled:bg-gray-100 disabled:text-gray-500\"\n disabled\n data-testid=\"accounting-accounts-form-type\"\n >\n <option v-for=\"option in TYPE_OPTIONS\" :key=\"option\" :value=\"option\">\n {{ t(`pluginAccounting.accounts.typeOption.${option}`) }}\n </option>\n </select>\n </label>\n </div>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n <span\n >{{ t(\"pluginAccounting.accounts.columnNote\") }} <span class=\"text-gray-400\">{{ t(\"pluginAccounting.accounts.noteOptional\") }}</span></span\n >\n <input v-model=\"local.note\" type=\"text\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-accounts-form-note\" />\n </label>\n <p v-if=\"!isNew\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.accounts.codeReadOnlyHint\") }}</p>\n <!-- Always rendered with min-h to reserve a single line of space\n so the Save / Cancel buttons stay put as the message shows\n and clears. Field error wins over a stale parent error. -->\n <p class=\"text-xs text-red-500 min-h-[1rem]\" data-testid=\"accounting-accounts-form-error\">{{ fieldErrorMessage ?? error ?? \"\" }}</p>\n <div class=\"flex justify-end gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-accounts-form-cancel\"\n @click=\"emit('cancel')\"\n >\n {{ t(\"pluginAccounting.accounts.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-2.5 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"busy\"\n data-testid=\"accounting-accounts-form-save\"\n >\n {{ busy ? t(\"pluginAccounting.accounts.saving\") : t(\"pluginAccounting.accounts.save\") }}\n </button>\n </div>\n </form>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, reactive, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type { Account, AccountType } from \"../api\";\nimport type { AccountDraft } from \"./accountDraft\";\nimport { ACCOUNT_TYPE_PREFIX } from \"./accountNumbering\";\nimport { validateCodeField, validateNameField, type AccountValidationError, type CodeValidationError, type NameValidationError } from \"./accountValidation\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n draft: AccountDraft;\n isNew: boolean;\n busy: boolean;\n error: string | null;\n existingAccounts: readonly Account[];\n}>();\nconst emit = defineEmits<{ save: [draft: AccountDraft]; cancel: [] }>();\n\nconst TYPE_OPTIONS: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\n\nconst VALIDATION_MESSAGE_KEYS: Record<AccountValidationError, string> = {\n emptyCode: \"pluginAccounting.accounts.errorEmptyCode\",\n reservedCode: \"pluginAccounting.accounts.errorReservedCode\",\n invalidCodeFormat: \"pluginAccounting.accounts.errorInvalidCodeFormat\",\n codeTypeMismatch: \"pluginAccounting.accounts.errorCodeTypeMismatch\",\n emptyName: \"pluginAccounting.accounts.errorEmptyName\",\n duplicateCode: \"pluginAccounting.accounts.errorDuplicateCode\",\n duplicateName: \"pluginAccounting.accounts.errorDuplicateName\",\n};\n\n// Local copy so the parent's `draft` ref stays untouched until the\n// user clicks Save. Cancelling reverts cleanly because the parent\n// just discards its draft.\nconst local = reactive<AccountDraft>({ ...props.draft });\nconst nameInput = ref<HTMLInputElement | null>(null);\n\n// Track which fields the user has interacted with so we can suppress\n// \"empty required\" errors on a freshly-opened editor (the suggested\n// code is always valid, but a brand-new account starts with an empty\n// name — flashing red before the user has typed would be noise).\n// Format / collision errors fire immediately because they only\n// happen when the user has actually entered something.\nconst codeTouched = ref(false);\nconst nameTouched = ref(false);\n\nconst codePrefix = computed(() => String(ACCOUNT_TYPE_PREFIX[local.type]));\n\n// Two-way binding for the trailing 3 digits. The full code\n// (`local.code`) remains the source of truth; the trailing slice is\n// derived. Non-digits and overflow are stripped on input so the\n// downstream validator only ever sees a clean 4-digit candidate.\nconst codeTrailing = computed({\n get: () => {\n const { code } = local;\n if (code.startsWith(codePrefix.value)) return code.slice(codePrefix.value.length);\n return code;\n },\n set: (val: string) => {\n const cleaned = val.replace(/\\D/g, \"\").slice(0, 3);\n local.code = codePrefix.value + cleaned;\n },\n});\n\nconst codeError = computed<CodeValidationError | null>(() => {\n const result = validateCodeField(local, props.existingAccounts, props.isNew);\n if (result === \"emptyCode\" && !codeTouched.value) return null;\n return result;\n});\n\nconst nameError = computed<NameValidationError | null>(() => {\n const result = validateNameField(local, props.existingAccounts, props.isNew);\n // For NEW accounts the empty-name field is invalid from the moment\n // the editor opens — flag it red right away to communicate the\n // requirement. For edits, the name is non-empty on open; only flag\n // emptyName once the user has actively cleared it (post-touch).\n if (result === \"emptyName\" && !nameTouched.value && !props.isNew) return null;\n return result;\n});\n\nconst fieldErrorMessage = computed<string | null>(() => {\n const code = codeError.value;\n if (code !== null) return t(VALIDATION_MESSAGE_KEYS[code]);\n const name = nameError.value;\n if (name !== null) return t(VALIDATION_MESSAGE_KEYS[name]);\n return null;\n});\n\n// Re-sync when the parent swaps which account is being edited\n// (e.g. user clicks Edit on a different row without first\n// cancelling). Single watcher rather than per-field copy to keep\n// behaviour boringly predictable.\nwatch(\n () => props.draft,\n (next) => {\n local.code = next.code;\n local.name = next.name;\n local.type = next.type;\n local.note = next.note;\n codeTouched.value = false;\n nameTouched.value = false;\n },\n);\n\nonMounted(() => {\n // Land the cursor in the field the user actually has to fill in:\n // - new accounts: code is suggested and type is locked, so\n // Name is the only non-decorative input.\n // - edits: code is disabled, type is rarely the reason for\n // editing — Name is still the most likely target. Keeping\n // focus consistent across new/edit avoids surprise.\n void nextTick(() => nameInput.value?.focus());\n});\n\nfunction onSubmit(): void {\n // Surface any latent empty-required errors that were suppressed\n // pre-touch — clicking Save is intent enough to want the red\n // border, even if the user never typed in the field.\n codeTouched.value = true;\n nameTouched.value = true;\n emit(\"save\", { code: local.code, name: local.name, type: local.type, note: local.note });\n}\n</script>\n","<template>\n <!-- Inline editor used by AccountsModal both for \"Edit\" on an\n existing row and per-section \"+ Add\" buttons. The parent\n owns the open/closed state and the draft instance — this\n component is dumb, but it runs realtime per-field validation\n (red border) so the user gets feedback before clicking Save. -->\n <form\n class=\"flex flex-col gap-2 p-2 border border-blue-200 bg-blue-50/40 rounded text-sm\"\n :data-testid=\"isNew ? 'accounting-accounts-form-new' : `accounting-accounts-form-edit-${draft.code}`\"\n @submit.prevent=\"onSubmit\"\n >\n <div class=\"flex flex-wrap gap-2\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-28\">\n {{ t(\"pluginAccounting.accounts.columnCode\") }}\n <!-- New accounts: leading digit is fixed by type, so the\n editable input is restricted to the trailing 3 digits.\n The prefix span communicates the rule visually without\n needing a separate help string. -->\n <!-- The trailing-3-digit input has `outline-none bg-transparent`\n so the prefix span and the editable digits read as one\n pill. That removes the browser's default focus indicator,\n so we surface it on the wrapper via `focus-within:ring-1`\n — same shape as the name input below — to keep the field\n keyboard-discoverable (#1115 review). -->\n <div\n v-if=\"isNew\"\n :class=\"[\n 'flex items-stretch h-8 rounded border bg-white text-sm font-mono overflow-hidden',\n codeError ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-300 focus-within:ring-1 focus-within:ring-blue-500',\n ]\"\n >\n <span\n class=\"px-2 flex items-center bg-gray-100 text-gray-500 border-r border-gray-200 select-none\"\n data-testid=\"accounting-accounts-form-code-prefix\"\n >{{ codePrefix }}</span\n >\n <input\n v-model=\"codeTrailing\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"3\"\n pattern=\"\\d{3}\"\n class=\"px-2 grow w-0 outline-none bg-transparent\"\n data-testid=\"accounting-accounts-form-code\"\n @input=\"codeTouched = true\"\n />\n </div>\n <!-- Edit: code is immutable, so we display the actual stored\n value as a single disabled field rather than splitting\n prefix + trailing (legacy non-4-digit codes would\n otherwise be misrendered). -->\n <input\n v-else\n v-model=\"local.code\"\n type=\"text\"\n disabled\n class=\"h-8 px-2 rounded border border-gray-300 text-sm font-mono bg-gray-100 text-gray-500\"\n data-testid=\"accounting-accounts-form-code\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 grow min-w-[10rem]\">\n {{ t(\"pluginAccounting.accounts.columnName\") }}\n <input\n ref=\"nameInput\"\n v-model=\"local.name\"\n type=\"text\"\n :class=\"[\n 'h-8 px-2 rounded border text-sm focus:outline-none',\n nameError ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-300 focus:ring-1 focus:ring-blue-500',\n ]\"\n data-testid=\"accounting-accounts-form-name\"\n @input=\"nameTouched = true\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-32\">\n {{ t(\"pluginAccounting.accounts.columnType\") }}\n <!-- Type is locked in both modes:\n - NEW: the per-category \"+ Add\" button already chose\n it, and the suggested code is keyed off it.\n - EDIT: the type is part of the account's identity (it\n drives section placement, the code-prefix rule, and\n report categorization); changing it after the fact\n leads to surprising downstream effects. -->\n <select\n v-model=\"local.type\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white disabled:bg-gray-100 disabled:text-gray-500\"\n disabled\n data-testid=\"accounting-accounts-form-type\"\n >\n <option v-for=\"option in TYPE_OPTIONS\" :key=\"option\" :value=\"option\">\n {{ t(`pluginAccounting.accounts.typeOption.${option}`) }}\n </option>\n </select>\n </label>\n </div>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n <span\n >{{ t(\"pluginAccounting.accounts.columnNote\") }} <span class=\"text-gray-400\">{{ t(\"pluginAccounting.accounts.noteOptional\") }}</span></span\n >\n <input v-model=\"local.note\" type=\"text\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-accounts-form-note\" />\n </label>\n <p v-if=\"!isNew\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.accounts.codeReadOnlyHint\") }}</p>\n <!-- Always rendered with min-h to reserve a single line of space\n so the Save / Cancel buttons stay put as the message shows\n and clears. Field error wins over a stale parent error. -->\n <p class=\"text-xs text-red-500 min-h-[1rem]\" data-testid=\"accounting-accounts-form-error\">{{ fieldErrorMessage ?? error ?? \"\" }}</p>\n <div class=\"flex justify-end gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-accounts-form-cancel\"\n @click=\"emit('cancel')\"\n >\n {{ t(\"pluginAccounting.accounts.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-2.5 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"busy\"\n data-testid=\"accounting-accounts-form-save\"\n >\n {{ busy ? t(\"pluginAccounting.accounts.saving\") : t(\"pluginAccounting.accounts.save\") }}\n </button>\n </div>\n </form>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, reactive, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type { Account, AccountType } from \"../api\";\nimport type { AccountDraft } from \"./accountDraft\";\nimport { ACCOUNT_TYPE_PREFIX } from \"./accountNumbering\";\nimport { validateCodeField, validateNameField, type AccountValidationError, type CodeValidationError, type NameValidationError } from \"./accountValidation\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n draft: AccountDraft;\n isNew: boolean;\n busy: boolean;\n error: string | null;\n existingAccounts: readonly Account[];\n}>();\nconst emit = defineEmits<{ save: [draft: AccountDraft]; cancel: [] }>();\n\nconst TYPE_OPTIONS: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\n\nconst VALIDATION_MESSAGE_KEYS: Record<AccountValidationError, string> = {\n emptyCode: \"pluginAccounting.accounts.errorEmptyCode\",\n reservedCode: \"pluginAccounting.accounts.errorReservedCode\",\n invalidCodeFormat: \"pluginAccounting.accounts.errorInvalidCodeFormat\",\n codeTypeMismatch: \"pluginAccounting.accounts.errorCodeTypeMismatch\",\n emptyName: \"pluginAccounting.accounts.errorEmptyName\",\n duplicateCode: \"pluginAccounting.accounts.errorDuplicateCode\",\n duplicateName: \"pluginAccounting.accounts.errorDuplicateName\",\n};\n\n// Local copy so the parent's `draft` ref stays untouched until the\n// user clicks Save. Cancelling reverts cleanly because the parent\n// just discards its draft.\nconst local = reactive<AccountDraft>({ ...props.draft });\nconst nameInput = ref<HTMLInputElement | null>(null);\n\n// Track which fields the user has interacted with so we can suppress\n// \"empty required\" errors on a freshly-opened editor (the suggested\n// code is always valid, but a brand-new account starts with an empty\n// name — flashing red before the user has typed would be noise).\n// Format / collision errors fire immediately because they only\n// happen when the user has actually entered something.\nconst codeTouched = ref(false);\nconst nameTouched = ref(false);\n\nconst codePrefix = computed(() => String(ACCOUNT_TYPE_PREFIX[local.type]));\n\n// Two-way binding for the trailing 3 digits. The full code\n// (`local.code`) remains the source of truth; the trailing slice is\n// derived. Non-digits and overflow are stripped on input so the\n// downstream validator only ever sees a clean 4-digit candidate.\nconst codeTrailing = computed({\n get: () => {\n const { code } = local;\n if (code.startsWith(codePrefix.value)) return code.slice(codePrefix.value.length);\n return code;\n },\n set: (val: string) => {\n const cleaned = val.replace(/\\D/g, \"\").slice(0, 3);\n local.code = codePrefix.value + cleaned;\n },\n});\n\nconst codeError = computed<CodeValidationError | null>(() => {\n const result = validateCodeField(local, props.existingAccounts, props.isNew);\n if (result === \"emptyCode\" && !codeTouched.value) return null;\n return result;\n});\n\nconst nameError = computed<NameValidationError | null>(() => {\n const result = validateNameField(local, props.existingAccounts, props.isNew);\n // For NEW accounts the empty-name field is invalid from the moment\n // the editor opens — flag it red right away to communicate the\n // requirement. For edits, the name is non-empty on open; only flag\n // emptyName once the user has actively cleared it (post-touch).\n if (result === \"emptyName\" && !nameTouched.value && !props.isNew) return null;\n return result;\n});\n\nconst fieldErrorMessage = computed<string | null>(() => {\n const code = codeError.value;\n if (code !== null) return t(VALIDATION_MESSAGE_KEYS[code]);\n const name = nameError.value;\n if (name !== null) return t(VALIDATION_MESSAGE_KEYS[name]);\n return null;\n});\n\n// Re-sync when the parent swaps which account is being edited\n// (e.g. user clicks Edit on a different row without first\n// cancelling). Single watcher rather than per-field copy to keep\n// behaviour boringly predictable.\nwatch(\n () => props.draft,\n (next) => {\n local.code = next.code;\n local.name = next.name;\n local.type = next.type;\n local.note = next.note;\n codeTouched.value = false;\n nameTouched.value = false;\n },\n);\n\nonMounted(() => {\n // Land the cursor in the field the user actually has to fill in:\n // - new accounts: code is suggested and type is locked, so\n // Name is the only non-decorative input.\n // - edits: code is disabled, type is rarely the reason for\n // editing — Name is still the most likely target. Keeping\n // focus consistent across new/edit avoids surprise.\n void nextTick(() => nameInput.value?.focus());\n});\n\nfunction onSubmit(): void {\n // Surface any latent empty-required errors that were suppressed\n // pre-touch — clicking Save is intent enough to want the red\n // border, even if the user never typed in the field.\n codeTouched.value = true;\n nameTouched.value = true;\n emit(\"save\", { code: local.code, name: local.name, type: local.type, note: local.note });\n}\n</script>\n","<template>\n <!-- Manage-accounts modal. Opened from JournalEntryForm and\n OpeningBalancesForm. Lists the current chart of accounts\n grouped by type, with inline add / edit. Stays open across\n saves so the user can fix several accounts in a row. -->\n <div\n class=\"fixed inset-0 z-50 bg-black/20 flex items-center justify-center\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"accounting-accounts-modal-title\"\n data-testid=\"accounting-accounts-modal\"\n @click.self=\"onBackdropClick\"\n @keydown.esc=\"emit('close')\"\n >\n <div class=\"bg-white rounded shadow-lg w-[32rem] max-h-[80vh] flex flex-col\">\n <header class=\"flex items-center justify-between px-4 py-2 border-b border-gray-200 shrink-0\">\n <h3 id=\"accounting-accounts-modal-title\" class=\"text-base font-semibold\">{{ t(\"pluginAccounting.accounts.modalTitle\") }}</h3>\n <button\n ref=\"closeButton\"\n type=\"button\"\n class=\"h-8 w-8 flex items-center justify-center rounded text-gray-500 hover:bg-gray-100\"\n data-testid=\"accounting-accounts-close\"\n :aria-label=\"t('pluginAccounting.common.cancel')\"\n @click=\"emit('close')\"\n >\n <span class=\"material-icons text-base\">close</span>\n </button>\n </header>\n <div class=\"flex-1 overflow-auto px-4 py-3 flex flex-col gap-3\">\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-accounts-success\">{{ successMessage }}</p>\n <p v-if=\"toggleError\" class=\"text-xs text-red-500\" data-testid=\"accounting-accounts-toggle-error\">{{ toggleError }}</p>\n <section v-for=\"group in groups\" :key=\"group.type\" class=\"flex flex-col gap-1\">\n <h4 class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ t(`pluginAccounting.accounts.sectionTitle.${group.type}`) }}</h4>\n <div v-if=\"group.accounts.length === 0\" class=\"text-xs text-gray-400 italic px-1\">{{ t(\"pluginAccounting.common.empty\") }}</div>\n <template v-for=\"account in group.accounts\" :key=\"account.code\">\n <AccountRow v-if=\"editingCode !== account.code\" :account=\"account\" @edit=\"onEdit(account)\" @toggle-active=\"onToggleActive(account)\" />\n <AccountEditor\n v-else\n :draft=\"draft\"\n :is-new=\"false\"\n :busy=\"saving\"\n :error=\"error\"\n :existing-accounts=\"accounts\"\n @save=\"onSave\"\n @cancel=\"onCancelEditor\"\n />\n </template>\n <div v-if=\"addingNew && draft.type === group.type\" :ref=\"(node) => bindNewEditor(node, group.type)\">\n <AccountEditor :draft=\"draft\" is-new :busy=\"saving\" :error=\"error\" :existing-accounts=\"accounts\" @save=\"onSave\" @cancel=\"onCancelEditor\" />\n </div>\n <button\n v-else\n type=\"button\"\n class=\"self-start h-8 px-2.5 flex items-center gap-1 rounded text-xs text-gray-600 hover:bg-gray-100\"\n :data-testid=\"`accounting-accounts-add-${group.type}`\"\n @click=\"onAdd(group.type)\"\n >\n <span class=\"material-icons text-sm\">add</span>\n <span>{{ t(\"pluginAccounting.accounts.addToCategory\", { type: t(`pluginAccounting.accounts.typeOption.${group.type}`) }) }}</span>\n </button>\n </section>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, onUnmounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { upsertAccount, type Account, type AccountType } from \"../api\";\nimport AccountRow from \"./AccountRow.vue\";\nimport AccountEditor from \"./AccountEditor.vue\";\nimport type { AccountDraft } from \"./accountDraft\";\nimport { validateAccountDraft, type AccountValidationError } from \"./accountValidation\";\nimport { suggestNextCode } from \"./accountNumbering\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[] }>();\nconst emit = defineEmits<{ close: []; changed: [] }>();\n\n// Order matches conventional financial-statement layout (B/S then\n// P/L). Section titles are pulled from i18n via the literal type\n// keys, so this array drives both ordering and visibility.\nconst ACCOUNT_TYPES: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\nconst SUCCESS_FADE_MS = 2500;\n\nconst VALIDATION_MESSAGE_KEYS: Record<AccountValidationError, string> = {\n emptyCode: \"pluginAccounting.accounts.errorEmptyCode\",\n reservedCode: \"pluginAccounting.accounts.errorReservedCode\",\n invalidCodeFormat: \"pluginAccounting.accounts.errorInvalidCodeFormat\",\n codeTypeMismatch: \"pluginAccounting.accounts.errorCodeTypeMismatch\",\n emptyName: \"pluginAccounting.accounts.errorEmptyName\",\n duplicateCode: \"pluginAccounting.accounts.errorDuplicateCode\",\n duplicateName: \"pluginAccounting.accounts.errorDuplicateName\",\n};\n\ninterface AccountGroup {\n type: AccountType;\n accounts: Account[];\n}\n\nconst groups = computed<AccountGroup[]>(() =>\n ACCOUNT_TYPES.map((type) => ({\n type,\n accounts: props.accounts\n .filter((account) => account.type === type)\n .slice()\n .sort(byCode),\n })),\n);\n\nfunction byCode(left: Account, right: Account): number {\n return left.code.localeCompare(right.code);\n}\n\nconst editingCode = ref<string | null>(null);\nconst addingNew = ref(false);\nconst draft = ref<AccountDraft>(emptyDraft(\"asset\"));\nconst saving = ref(false);\nconst error = ref<string | null>(null);\n// Toggle (Deactivate / Reactivate) keeps its own state. Sharing\n// `saving` / `error` with the editor would (a) hide a toggle\n// failure when no editor is mounted to render `:error`, and (b)\n// blank out an in-progress editor's validation message and\n// freeze its Save button when the user fires a toggle on a\n// different row.\nconst toggleSaving = ref(false);\nconst toggleError = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\nconst closeButton = ref<HTMLButtonElement | null>(null);\nconst newEditorWrapper = ref<HTMLDivElement | null>(null);\nlet successTimer: ReturnType<typeof setTimeout> | null = null;\n\nfunction emptyDraft(type: AccountType): AccountDraft {\n return { code: \"\", name: \"\", type, note: \"\" };\n}\n\nfunction draftForNew(type: AccountType): AccountDraft {\n return { code: suggestNextCode(type, props.accounts), name: \"\", type, note: \"\" };\n}\n\n// Vue's `:ref` on a v-for-style element gives us back either the\n// node or null (on unmount). We only want to capture the editor\n// belonging to the section that owns the current draft, so the\n// section type is checked here rather than relying on the order\n// in which Vue invokes the function refs.\nfunction bindNewEditor(node: Element | object | null, sectionType: AccountType): void {\n if (sectionType !== draft.value.type) return;\n newEditorWrapper.value = (node as HTMLDivElement | null) ?? null;\n}\n\nfunction onEdit(account: Account): void {\n // Collapse any other editor first so only one is open at a time.\n addingNew.value = false;\n error.value = null;\n draft.value = { code: account.code, name: account.name, type: account.type, note: account.note ?? \"\" };\n editingCode.value = account.code;\n}\n\nfunction onAdd(type: AccountType): void {\n editingCode.value = null;\n error.value = null;\n draft.value = draftForNew(type);\n addingNew.value = true;\n // Scroll the new in-place editor into view in case the section\n // sits below the visible viewport — opening the editor without\n // scrolling would leave the user staring at unchanged content\n // above the fold.\n void nextTick(() => {\n newEditorWrapper.value?.scrollIntoView({ behavior: \"smooth\", block: \"nearest\" });\n });\n}\n\nfunction onCancelEditor(): void {\n editingCode.value = null;\n addingNew.value = false;\n error.value = null;\n draft.value = emptyDraft(\"asset\");\n}\n\nfunction validateDraft(next: AccountDraft, isNew: boolean): string | null {\n const code = validateAccountDraft(next, props.accounts, isNew);\n return code === null ? null : t(VALIDATION_MESSAGE_KEYS[code]);\n}\n\nasync function onSave(next: AccountDraft): Promise<void> {\n if (saving.value) return;\n const isNew = addingNew.value;\n const validation = validateDraft(next, isNew);\n if (validation !== null) {\n error.value = validation;\n return;\n }\n saving.value = true;\n error.value = null;\n try {\n const account: Account = {\n code: next.code.trim(),\n name: next.name.trim(),\n type: next.type,\n };\n const note = next.note.trim();\n if (note.length > 0) account.note = note;\n // Preserve the existing active flag on edit — the editor\n // doesn't surface the field, so reading from props.accounts\n // is the only place the truth lives.\n if (!isNew) {\n const existing = props.accounts.find((entry) => entry.code === account.code);\n if (existing?.active === false) account.active = false;\n }\n const result = await upsertAccount(account, props.bookId);\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n onCancelEditor();\n showSuccess(t(\"pluginAccounting.accounts.success\"));\n emit(\"changed\");\n } catch (err) {\n // apiPost normally folds network / HTTP failures into\n // result.ok=false, so this is a belt-and-braces guard against\n // a runtime failure that would otherwise leave the Save button\n // stuck on \"Saving…\".\n error.value = errorMessage(err);\n } finally {\n saving.value = false;\n }\n}\n\nasync function onToggleActive(account: Account): Promise<void> {\n // No confirm dialog: deactivation hides the account from the\n // entry/ledger dropdowns but is fully reversible via Reactivate\n // on the same row, and historical entries are unaffected. A\n // confirm prompt was over-protective for an action that's a\n // single click to undo.\n //\n // Toggle uses its own `toggleSaving` / `toggleError` refs rather\n // than the AccountEditor's shared `saving` / `error` so that a\n // toggle failure still surfaces (via the toggle banner) when no\n // editor is mounted to render `:error`.\n if (toggleSaving.value) return;\n // Dismiss any open editor — the row about to (de)activate may be\n // the same one being edited, and even when it isn't, the user has\n // shifted attention to the toggle. Unsaved edits are dropped per\n // product call: reopening Edit is one click.\n onCancelEditor();\n const willDeactivate = account.active !== false;\n toggleSaving.value = true;\n toggleError.value = null;\n try {\n const next: Account = {\n code: account.code,\n name: account.name,\n type: account.type,\n };\n if (account.note !== undefined && account.note.length > 0) next.note = account.note;\n // Send the active flag explicitly so the server can tell\n // \"user wants to (de)activate\" apart from \"user is editing\n // and didn't mention active\" — the latter inherits the\n // existing flag and would otherwise turn Reactivate into a\n // no-op.\n next.active = !willDeactivate;\n const result = await upsertAccount(next, props.bookId);\n if (!result.ok) {\n toggleError.value = result.error;\n return;\n }\n emit(\"changed\");\n } catch (err) {\n toggleError.value = errorMessage(err);\n } finally {\n toggleSaving.value = false;\n }\n}\n\nfunction showSuccess(message: string): void {\n successMessage.value = message;\n if (successTimer !== null) clearTimeout(successTimer);\n successTimer = setTimeout(() => {\n successMessage.value = null;\n successTimer = null;\n }, SUCCESS_FADE_MS);\n}\n\nfunction onBackdropClick(): void {\n emit(\"close\");\n}\n\nonMounted(() => {\n // Initial focus on the close button so Esc / Tab work\n // immediately and the user isn't dropped into an editor field\n // they didn't ask for.\n void nextTick(() => closeButton.value?.focus());\n});\n\nonUnmounted(() => {\n if (successTimer !== null) clearTimeout(successTimer);\n});\n</script>\n","<template>\n <!-- Manage-accounts modal. Opened from JournalEntryForm and\n OpeningBalancesForm. Lists the current chart of accounts\n grouped by type, with inline add / edit. Stays open across\n saves so the user can fix several accounts in a row. -->\n <div\n class=\"fixed inset-0 z-50 bg-black/20 flex items-center justify-center\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"accounting-accounts-modal-title\"\n data-testid=\"accounting-accounts-modal\"\n @click.self=\"onBackdropClick\"\n @keydown.esc=\"emit('close')\"\n >\n <div class=\"bg-white rounded shadow-lg w-[32rem] max-h-[80vh] flex flex-col\">\n <header class=\"flex items-center justify-between px-4 py-2 border-b border-gray-200 shrink-0\">\n <h3 id=\"accounting-accounts-modal-title\" class=\"text-base font-semibold\">{{ t(\"pluginAccounting.accounts.modalTitle\") }}</h3>\n <button\n ref=\"closeButton\"\n type=\"button\"\n class=\"h-8 w-8 flex items-center justify-center rounded text-gray-500 hover:bg-gray-100\"\n data-testid=\"accounting-accounts-close\"\n :aria-label=\"t('pluginAccounting.common.cancel')\"\n @click=\"emit('close')\"\n >\n <span class=\"material-icons text-base\">close</span>\n </button>\n </header>\n <div class=\"flex-1 overflow-auto px-4 py-3 flex flex-col gap-3\">\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-accounts-success\">{{ successMessage }}</p>\n <p v-if=\"toggleError\" class=\"text-xs text-red-500\" data-testid=\"accounting-accounts-toggle-error\">{{ toggleError }}</p>\n <section v-for=\"group in groups\" :key=\"group.type\" class=\"flex flex-col gap-1\">\n <h4 class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ t(`pluginAccounting.accounts.sectionTitle.${group.type}`) }}</h4>\n <div v-if=\"group.accounts.length === 0\" class=\"text-xs text-gray-400 italic px-1\">{{ t(\"pluginAccounting.common.empty\") }}</div>\n <template v-for=\"account in group.accounts\" :key=\"account.code\">\n <AccountRow v-if=\"editingCode !== account.code\" :account=\"account\" @edit=\"onEdit(account)\" @toggle-active=\"onToggleActive(account)\" />\n <AccountEditor\n v-else\n :draft=\"draft\"\n :is-new=\"false\"\n :busy=\"saving\"\n :error=\"error\"\n :existing-accounts=\"accounts\"\n @save=\"onSave\"\n @cancel=\"onCancelEditor\"\n />\n </template>\n <div v-if=\"addingNew && draft.type === group.type\" :ref=\"(node) => bindNewEditor(node, group.type)\">\n <AccountEditor :draft=\"draft\" is-new :busy=\"saving\" :error=\"error\" :existing-accounts=\"accounts\" @save=\"onSave\" @cancel=\"onCancelEditor\" />\n </div>\n <button\n v-else\n type=\"button\"\n class=\"self-start h-8 px-2.5 flex items-center gap-1 rounded text-xs text-gray-600 hover:bg-gray-100\"\n :data-testid=\"`accounting-accounts-add-${group.type}`\"\n @click=\"onAdd(group.type)\"\n >\n <span class=\"material-icons text-sm\">add</span>\n <span>{{ t(\"pluginAccounting.accounts.addToCategory\", { type: t(`pluginAccounting.accounts.typeOption.${group.type}`) }) }}</span>\n </button>\n </section>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, onUnmounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { upsertAccount, type Account, type AccountType } from \"../api\";\nimport AccountRow from \"./AccountRow.vue\";\nimport AccountEditor from \"./AccountEditor.vue\";\nimport type { AccountDraft } from \"./accountDraft\";\nimport { validateAccountDraft, type AccountValidationError } from \"./accountValidation\";\nimport { suggestNextCode } from \"./accountNumbering\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[] }>();\nconst emit = defineEmits<{ close: []; changed: [] }>();\n\n// Order matches conventional financial-statement layout (B/S then\n// P/L). Section titles are pulled from i18n via the literal type\n// keys, so this array drives both ordering and visibility.\nconst ACCOUNT_TYPES: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\nconst SUCCESS_FADE_MS = 2500;\n\nconst VALIDATION_MESSAGE_KEYS: Record<AccountValidationError, string> = {\n emptyCode: \"pluginAccounting.accounts.errorEmptyCode\",\n reservedCode: \"pluginAccounting.accounts.errorReservedCode\",\n invalidCodeFormat: \"pluginAccounting.accounts.errorInvalidCodeFormat\",\n codeTypeMismatch: \"pluginAccounting.accounts.errorCodeTypeMismatch\",\n emptyName: \"pluginAccounting.accounts.errorEmptyName\",\n duplicateCode: \"pluginAccounting.accounts.errorDuplicateCode\",\n duplicateName: \"pluginAccounting.accounts.errorDuplicateName\",\n};\n\ninterface AccountGroup {\n type: AccountType;\n accounts: Account[];\n}\n\nconst groups = computed<AccountGroup[]>(() =>\n ACCOUNT_TYPES.map((type) => ({\n type,\n accounts: props.accounts\n .filter((account) => account.type === type)\n .slice()\n .sort(byCode),\n })),\n);\n\nfunction byCode(left: Account, right: Account): number {\n return left.code.localeCompare(right.code);\n}\n\nconst editingCode = ref<string | null>(null);\nconst addingNew = ref(false);\nconst draft = ref<AccountDraft>(emptyDraft(\"asset\"));\nconst saving = ref(false);\nconst error = ref<string | null>(null);\n// Toggle (Deactivate / Reactivate) keeps its own state. Sharing\n// `saving` / `error` with the editor would (a) hide a toggle\n// failure when no editor is mounted to render `:error`, and (b)\n// blank out an in-progress editor's validation message and\n// freeze its Save button when the user fires a toggle on a\n// different row.\nconst toggleSaving = ref(false);\nconst toggleError = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\nconst closeButton = ref<HTMLButtonElement | null>(null);\nconst newEditorWrapper = ref<HTMLDivElement | null>(null);\nlet successTimer: ReturnType<typeof setTimeout> | null = null;\n\nfunction emptyDraft(type: AccountType): AccountDraft {\n return { code: \"\", name: \"\", type, note: \"\" };\n}\n\nfunction draftForNew(type: AccountType): AccountDraft {\n return { code: suggestNextCode(type, props.accounts), name: \"\", type, note: \"\" };\n}\n\n// Vue's `:ref` on a v-for-style element gives us back either the\n// node or null (on unmount). We only want to capture the editor\n// belonging to the section that owns the current draft, so the\n// section type is checked here rather than relying on the order\n// in which Vue invokes the function refs.\nfunction bindNewEditor(node: Element | object | null, sectionType: AccountType): void {\n if (sectionType !== draft.value.type) return;\n newEditorWrapper.value = (node as HTMLDivElement | null) ?? null;\n}\n\nfunction onEdit(account: Account): void {\n // Collapse any other editor first so only one is open at a time.\n addingNew.value = false;\n error.value = null;\n draft.value = { code: account.code, name: account.name, type: account.type, note: account.note ?? \"\" };\n editingCode.value = account.code;\n}\n\nfunction onAdd(type: AccountType): void {\n editingCode.value = null;\n error.value = null;\n draft.value = draftForNew(type);\n addingNew.value = true;\n // Scroll the new in-place editor into view in case the section\n // sits below the visible viewport — opening the editor without\n // scrolling would leave the user staring at unchanged content\n // above the fold.\n void nextTick(() => {\n newEditorWrapper.value?.scrollIntoView({ behavior: \"smooth\", block: \"nearest\" });\n });\n}\n\nfunction onCancelEditor(): void {\n editingCode.value = null;\n addingNew.value = false;\n error.value = null;\n draft.value = emptyDraft(\"asset\");\n}\n\nfunction validateDraft(next: AccountDraft, isNew: boolean): string | null {\n const code = validateAccountDraft(next, props.accounts, isNew);\n return code === null ? null : t(VALIDATION_MESSAGE_KEYS[code]);\n}\n\nasync function onSave(next: AccountDraft): Promise<void> {\n if (saving.value) return;\n const isNew = addingNew.value;\n const validation = validateDraft(next, isNew);\n if (validation !== null) {\n error.value = validation;\n return;\n }\n saving.value = true;\n error.value = null;\n try {\n const account: Account = {\n code: next.code.trim(),\n name: next.name.trim(),\n type: next.type,\n };\n const note = next.note.trim();\n if (note.length > 0) account.note = note;\n // Preserve the existing active flag on edit — the editor\n // doesn't surface the field, so reading from props.accounts\n // is the only place the truth lives.\n if (!isNew) {\n const existing = props.accounts.find((entry) => entry.code === account.code);\n if (existing?.active === false) account.active = false;\n }\n const result = await upsertAccount(account, props.bookId);\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n onCancelEditor();\n showSuccess(t(\"pluginAccounting.accounts.success\"));\n emit(\"changed\");\n } catch (err) {\n // apiPost normally folds network / HTTP failures into\n // result.ok=false, so this is a belt-and-braces guard against\n // a runtime failure that would otherwise leave the Save button\n // stuck on \"Saving…\".\n error.value = errorMessage(err);\n } finally {\n saving.value = false;\n }\n}\n\nasync function onToggleActive(account: Account): Promise<void> {\n // No confirm dialog: deactivation hides the account from the\n // entry/ledger dropdowns but is fully reversible via Reactivate\n // on the same row, and historical entries are unaffected. A\n // confirm prompt was over-protective for an action that's a\n // single click to undo.\n //\n // Toggle uses its own `toggleSaving` / `toggleError` refs rather\n // than the AccountEditor's shared `saving` / `error` so that a\n // toggle failure still surfaces (via the toggle banner) when no\n // editor is mounted to render `:error`.\n if (toggleSaving.value) return;\n // Dismiss any open editor — the row about to (de)activate may be\n // the same one being edited, and even when it isn't, the user has\n // shifted attention to the toggle. Unsaved edits are dropped per\n // product call: reopening Edit is one click.\n onCancelEditor();\n const willDeactivate = account.active !== false;\n toggleSaving.value = true;\n toggleError.value = null;\n try {\n const next: Account = {\n code: account.code,\n name: account.name,\n type: account.type,\n };\n if (account.note !== undefined && account.note.length > 0) next.note = account.note;\n // Send the active flag explicitly so the server can tell\n // \"user wants to (de)activate\" apart from \"user is editing\n // and didn't mention active\" — the latter inherits the\n // existing flag and would otherwise turn Reactivate into a\n // no-op.\n next.active = !willDeactivate;\n const result = await upsertAccount(next, props.bookId);\n if (!result.ok) {\n toggleError.value = result.error;\n return;\n }\n emit(\"changed\");\n } catch (err) {\n toggleError.value = errorMessage(err);\n } finally {\n toggleSaving.value = false;\n }\n}\n\nfunction showSuccess(message: string): void {\n successMessage.value = message;\n if (successTimer !== null) clearTimeout(successTimer);\n successTimer = setTimeout(() => {\n successMessage.value = null;\n successTimer = null;\n }, SUCCESS_FADE_MS);\n}\n\nfunction onBackdropClick(): void {\n emit(\"close\");\n}\n\nonMounted(() => {\n // Initial focus on the close button so Esc / Tab work\n // immediately and the user isn't dropped into an editor field\n // they didn't ask for.\n void nextTick(() => closeButton.value?.focus());\n});\n\nonUnmounted(() => {\n if (successTimer !== null) clearTimeout(successTimer);\n});\n</script>\n","<template>\n <form class=\"flex flex-col gap-3\" data-testid=\"accounting-entry-form\" @submit.prevent=\"onSubmit\">\n <!-- Edit mode mounts inside the row's expanded detail panel,\n which already gives the user enough context (the row above\n shows date / kind / memo / lines). Hide the redundant\n \"Edit journal entry\" title there; the editBanner below\n still surfaces the void-and-replace consequence. -->\n <h3 v-if=\"!isEditing\" class=\"text-base font-semibold\">{{ t(\"pluginAccounting.entryForm.title\") }}</h3>\n <div class=\"flex flex-wrap gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.entryForm.dateLabel\") }}\n <input v-model=\"date\" type=\"date\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-entry-date\" />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 grow min-w-0\">\n {{ t(\"pluginAccounting.entryForm.memoLabel\") }}\n <input v-model=\"memo\" type=\"text\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-entry-memo\" />\n </label>\n </div>\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n <th v-if=\"anyTaxLine\" class=\"text-left py-1 px-2 w-40\">{{ t(\"pluginAccounting.entryForm.taxRegistrationIdLabel\") }}</th>\n <th class=\"py-1 px-2\"></th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(line, idx) in lines\" :key=\"idx\" class=\"border-b border-gray-100\">\n <td class=\"py-1 px-2\">\n <select\n v-model=\"line.accountCode\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm bg-white\"\n :data-testid=\"`accounting-entry-line-account-${idx}`\"\n >\n <option value=\"\">{{ DASH }}</option>\n <option v-for=\"account in selectableAccounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"line.debit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white\"\n :data-testid=\"`accounting-entry-line-debit-${idx}`\"\n @input=\"onDebitInput(line)\"\n />\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"line.credit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white\"\n :data-testid=\"`accounting-entry-line-credit-${idx}`\"\n @input=\"onCreditInput(line)\"\n />\n </td>\n <!-- Tax-registration ID column appears only when at least\n one line picks an input-tax account (14xx — see\n isTaxAccountCode). Within a column-visible row, the\n input itself only renders for the lines that actually\n pick a 14xx account; other lines render an empty cell\n so the row keeps its column alignment. -->\n <td v-if=\"anyTaxLine\" class=\"py-1 px-2\">\n <template v-if=\"isTaxLine(line)\">\n <input\n v-model=\"line.taxRegistrationId\"\n type=\"text\"\n :maxlength=\"MAX_TAX_REGISTRATION_ID_LENGTH\"\n :placeholder=\"t('pluginAccounting.entryForm.taxRegistrationIdPlaceholder')\"\n :class=\"[\n 'h-8 px-2 w-full rounded border text-sm font-mono bg-white focus:outline-none',\n isTaxRegistrationIdInvalid(line)\n ? 'border-red-500 ring-1 ring-red-500'\n : isTaxRegistrationIdMissing(line)\n ? 'border-amber-500 ring-1 ring-amber-500'\n : 'border-gray-300 focus:ring-1 focus:ring-blue-500',\n ]\"\n :data-testid=\"`accounting-entry-line-tax-registration-id-${idx}`\"\n :aria-describedby=\"isTaxRegistrationIdMissing(line) ? `accounting-entry-line-tax-registration-id-warning-${idx}` : undefined\"\n />\n <!-- Non-color cue for the amber border. Polite live\n region so screen readers are nudged when the\n user finishes typing an amount and the warning\n first appears, without interrupting other speech. -->\n <p\n v-if=\"isTaxRegistrationIdMissing(line)\"\n :id=\"`accounting-entry-line-tax-registration-id-warning-${idx}`\"\n class=\"text-xs text-amber-600 mt-1\"\n role=\"status\"\n aria-live=\"polite\"\n :data-testid=\"`accounting-entry-line-tax-registration-id-warning-${idx}`\"\n >\n {{ t(\"pluginAccounting.entryForm.taxRegistrationIdMissingWarning\") }}\n </p>\n </template>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <button v-if=\"lines.length > 2\" type=\"button\" class=\"text-xs text-red-500 hover:underline\" @click=\"lines.splice(idx, 1)\">\n {{ t(\"pluginAccounting.entryForm.removeLine\") }}\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n <div class=\"flex items-center justify-between\">\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-entry-add-line\"\n @click=\"addLine\"\n >\n <span class=\"material-icons text-base\">add</span>\n <span>{{ t(\"pluginAccounting.entryForm.addLine\") }}</span>\n </button>\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-entry-manage-accounts\"\n @click=\"showAccountsModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <span :class=\"balanced ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-entry-balance\">\n {{ balanced ? t(\"pluginAccounting.entryForm.balanced\") : t(\"pluginAccounting.entryForm.imbalance\", { amount: imbalanceText }) }}\n </span>\n </div>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-entry-error\">{{ error }}</p>\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-entry-success\">{{ successMessage }}</p>\n <div class=\"flex items-center justify-between gap-2\">\n <!-- editBanner sits on the action row in edit mode (instead\n of as a separate paragraph above the form) so the panel\n is shorter and the user reads the void-and-replace\n consequence right next to the button that triggers it. -->\n <p v-if=\"isEditing\" class=\"text-xs text-gray-500 flex-1 min-w-0\" data-testid=\"accounting-entry-edit-banner\">\n {{ t(\"pluginAccounting.entryForm.editBanner\") }}\n </p>\n <span v-else></span>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-3 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n :disabled=\"submitting\"\n data-testid=\"accounting-entry-cancel-edit\"\n @click=\"emit('cancel')\"\n >\n {{ t(\"pluginAccounting.common.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"!balanced || submitting || editLocked\"\n data-testid=\"accounting-entry-submit\"\n >\n {{ submitButtonLabel }}\n </button>\n </div>\n </div>\n </form>\n <!-- Sibling of the parent <form> on purpose: the modal renders\n its own <form @submit.prevent> for the inline editor, and\n nesting <form>s is invalid HTML that breaks Enter-key submit\n routing in some browsers. Vue 3 multi-root templates let us\n keep the markup flat with no wrapper div. -->\n <AccountsModal v-if=\"showAccountsModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showAccountsModal = false\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { addEntries, voidEntry, type Account, type JournalEntry, type JournalLine } from \"../api\";\nimport { formatAmount, inputStepFor, localDateString, countryHasFeature, type SupportedCountryCode } from \"../../shared\";\nimport { isTaxAccountCode } from \"./accountNumbering\";\nimport AccountsModal from \"./AccountsModal.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[]; currency: string; country?: SupportedCountryCode; entryToEdit?: JournalEntry | null }>();\nconst emit = defineEmits<{ submitted: []; cancel: [] }>();\n\nconst showAccountsModal = ref(false);\n\nconst DASH = \"—\";\n\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n return `${account.name} (${account.code})`;\n}\n\n// Hide deactivated accounts from the entry dropdown — accounting\n// integrity requires keeping them in the chart of accounts (any\n// historical journal line still references the code), but new\n// entries should not be able to land on a soft-deleted account.\nconst selectableAccounts = computed<Account[]>(() => props.accounts.filter((account) => account.active !== false));\nconst selectableAccountCodes = computed<Set<string>>(() => new Set(selectableAccounts.value.map((account) => account.code)));\n\ninterface FormLine {\n accountCode: string;\n debit: number | null;\n credit: number | null;\n taxRegistrationId: string;\n}\n\n// Mirrors server/accounting/journal.ts MAX_TAX_REGISTRATION_ID_LENGTH.\n// Duplicated rather than imported to keep the front-end bundle from\n// pulling in server modules (the existing client / server type alias\n// pattern in api.ts does the same — both sides own their copy of the\n// shape).\nconst MAX_TAX_REGISTRATION_ID_LENGTH = 32;\n\nfunction blankLine(): FormLine {\n return { accountCode: \"\", debit: null, credit: null, taxRegistrationId: \"\" };\n}\n\nfunction isTaxRegistrationIdInvalid(line: FormLine): boolean {\n return line.taxRegistrationId.trim().length > MAX_TAX_REGISTRATION_ID_LENGTH;\n}\n\nfunction isTaxLine(line: FormLine): boolean {\n return line.accountCode !== \"\" && isTaxAccountCode(line.accountCode);\n}\n\n// Soft warning: a postable tax line in a jurisdiction the role\n// prompt requires a counterparty registration number for (JP, EU,\n// GB, IN, AU, NZ, CA — see COUNTRY_FEATURES.warnMissingTaxRegistrationId)\n// gets an amber border + helper text when the field is blank. The\n// form lets the user post anyway (some suppliers genuinely won't\n// have one), but the silent-strip in `toApiLines` no longer goes\n// unnoticed. `function` declarations hoist, so calling `isPostable`\n// here is fine even though it appears later in the file.\nfunction isTaxRegistrationIdMissing(line: FormLine): boolean {\n if (!isTaxLine(line)) return false;\n if (!isPostable(line)) return false;\n if (!countryHasFeature(\"warnMissingTaxRegistrationId\", props.country)) return false;\n return line.taxRegistrationId.trim() === \"\";\n}\n\nconst date = ref(localDateString());\nconst memo = ref(\"\");\nconst lines = ref<FormLine[]>([blankLine(), blankLine()]);\nconst submitting = ref(false);\nconst error = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\n\nconst isEditing = computed<boolean>(() => Boolean(props.entryToEdit));\nconst submitButtonLabel = computed<string>(() => {\n if (submitting.value) {\n return isEditing.value ? t(\"pluginAccounting.entryForm.updating\") : t(\"pluginAccounting.entryForm.submitting\");\n }\n return isEditing.value ? t(\"pluginAccounting.entryForm.update\") : t(\"pluginAccounting.entryForm.submit\");\n});\n\n// One-shot lock: once the user has clicked Update on an edit, the\n// submit button is dead until they Cancel (or land on a different\n// entry). Edit = void + addEntries as two sequential calls; if the\n// void succeeds and the add fails, a second Submit would try to\n// void an already-voided original. We don't add retry plumbing\n// for that — policy is \"report the error, do not retry\". The user\n// cancels out and re-enters manually.\nconst editAttempted = ref(false);\nconst editLocked = computed(() => isEditing.value && editAttempted.value);\n\nfunction addLine(): void {\n lines.value.push(blankLine());\n}\n\n// Toggling ensures a single line never has both sides set — the\n// server validates this too, but doing it on input prevents a\n// confusing UX where the running total goes negative as the user\n// types.\nfunction onDebitInput(line: FormLine): void {\n if (line.debit !== null && line.debit !== 0) line.credit = null;\n}\nfunction onCreditInput(line: FormLine): void {\n if (line.credit !== null && line.credit !== 0) line.debit = null;\n}\n\n// Imbalance is computed off lines that are *postable* (have an\n// accountCode + a positive amount). Without that filter,\n// `balanced` could be `true` even when `toApiLines()` would drop a\n// row, and the user would hit a confusing \"needs ≥ 2 lines\" error\n// from the server on submit.\nconst imbalance = computed<number>(() => {\n let sum = 0;\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n if (isPositiveAmount(line.debit)) sum += line.debit;\n if (isPositiveAmount(line.credit)) sum -= line.credit;\n }\n return sum;\n});\nconst hasAtLeastTwoPostableLines = computed(() => {\n let count = 0;\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n count += 1;\n if (count >= 2) return true;\n }\n return false;\n});\n// Show the tax-registration ID column only when at least one line\n// targets a 14xx (input-tax) account; otherwise the column is\n// wasted space for the typical entry that has no input-tax line.\nconst anyTaxLine = computed(() => lines.value.some(isTaxLine));\nconst hasTaxRegistrationIdError = computed(() => lines.value.some(isTaxRegistrationIdInvalid));\nconst balanced = computed(() => Math.abs(imbalance.value) <= 0.005 && hasAtLeastTwoPostableLines.value && !hasTaxRegistrationIdError.value);\nconst imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));\nconst step = computed(() => inputStepFor(props.currency));\n\nfunction isPositiveAmount(value: unknown): value is number {\n // Robust against the empty string `v-model.number` produces when\n // the user clears a previously-typed field — `\"\" ?? 0 === 0` is\n // false so a naive truthy check would let the empty input through\n // as a phantom zero amount.\n return typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\nfunction isPostable(line: FormLine): boolean {\n if (!line.accountCode) return false;\n // Defence-in-depth against a code that was selectable when the\n // user picked it but got deactivated mid-edit. Hiding the\n // option from the dropdown alone isn't enough — the form's\n // `accountCode` value is sticky, so a stale selection would\n // still be POSTed if the user just hits submit. Gating\n // postability here also flows through to `balanced` and\n // `hasAtLeastTwoPostableLines`, so the submit button disables\n // and the user gets immediate feedback.\n if (!selectableAccountCodes.value.has(line.accountCode)) return false;\n return isPositiveAmount(line.debit) || isPositiveAmount(line.credit);\n}\n\nfunction toApiLines(): JournalLine[] {\n const out: JournalLine[] = [];\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n const apiLine: JournalLine = { accountCode: line.accountCode };\n if (isPositiveAmount(line.debit)) apiLine.debit = line.debit;\n if (isPositiveAmount(line.credit)) apiLine.credit = line.credit;\n // Only persist taxRegistrationId on tax-related lines —\n // otherwise a value typed against an earlier account choice\n // would leak through after the user switched the line to a\n // non-tax account (the input field disappears but the form\n // state lingers).\n if (isTaxLine(line)) {\n const trimmedTaxId = line.taxRegistrationId.trim();\n if (trimmedTaxId !== \"\") apiLine.taxRegistrationId = trimmedTaxId;\n }\n out.push(apiLine);\n }\n return out;\n}\n\nasync function onSubmit(): Promise<void> {\n if (submitting.value || !balanced.value || editLocked.value) return;\n submitting.value = true;\n error.value = null;\n successMessage.value = null;\n try {\n // Edit flow: void the original, then post the replacement.\n // Two sequential calls — not atomic, no retry. Marking\n // `editAttempted` *before* the void disables the submit button\n // for the rest of this edit session (the `editLocked` guard\n // and the button's :disabled both honour it), so a partial\n // failure can't trigger a second void on the already-voided\n // original. On any error: show the message, user must Cancel\n // and re-enter manually.\n const editingId = props.entryToEdit?.id;\n if (editingId) {\n editAttempted.value = true;\n const voidResult = await voidEntry({\n bookId: props.bookId,\n entryId: editingId,\n reason: t(\"pluginAccounting.entryForm.editVoidReason\"),\n });\n if (!voidResult.ok) {\n error.value = voidResult.error;\n return;\n }\n }\n const result = await addEntries({\n bookId: props.bookId,\n entries: [\n {\n date: date.value,\n memo: memo.value.trim() || undefined,\n lines: toApiLines(),\n ...(editingId ? { replacesEntryId: editingId } : {}),\n },\n ],\n });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n successMessage.value = editingId ? t(\"pluginAccounting.entryForm.editSuccess\") : t(\"pluginAccounting.entryForm.success\");\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n emit(\"submitted\");\n } catch (err) {\n // apiPost normally folds network / HTTP failures into\n // `result.ok = false`, so this branch should be rare. It's a\n // belt-and-braces guard against a runtime failure leaving the\n // submit button stuck — the user gets a visible error\n // instead of an unhandled rejection.\n error.value = errorMessage(err);\n } finally {\n submitting.value = false;\n }\n}\n\n// Reset the entire draft when bookId switches under us (rare but\n// possible via BookSwitcher while the form is open). Carrying the\n// previous book's lines and account codes into the new book is\n// the worst kind of silent failure — the new book might not even\n// have the same chart of accounts.\nwatch(\n () => props.bookId,\n () => {\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n date.value = localDateString();\n error.value = null;\n successMessage.value = null;\n },\n);\n\n// Edit mode: when the parent hands us an entry to edit, pre-fill\n// every field so the user can tweak and resubmit. When it clears\n// the prop (after submit / cancel / book switch), wipe back to a\n// blank draft so the next \"New entry\" tab visit is fresh. Mapping\n// `entry.lines` (the wire shape with optional `debit` / `credit`)\n// onto `FormLine` (which uses nullable numbers + a string\n// taxRegistrationId) is straightforward — pad missing optionals\n// to null / \"\".\nwatch(\n () => props.entryToEdit,\n (entry) => {\n error.value = null;\n successMessage.value = null;\n // Fresh edit (or exit from edit mode) → unlock the submit\n // button so the new edit session has a clean shot.\n editAttempted.value = false;\n if (!entry) {\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n date.value = localDateString();\n return;\n }\n date.value = entry.date;\n memo.value = entry.memo ?? \"\";\n lines.value = entry.lines.map((line) => ({\n accountCode: line.accountCode,\n debit: typeof line.debit === \"number\" ? line.debit : null,\n credit: typeof line.credit === \"number\" ? line.credit : null,\n taxRegistrationId: line.taxRegistrationId ?? \"\",\n }));\n if (lines.value.length < 2) {\n while (lines.value.length < 2) lines.value.push(blankLine());\n }\n },\n { immediate: true },\n);\n\n// If an account the user already picked gets deactivated mid-edit\n// (e.g. via the Manage Accounts modal in this form, or from\n// another tab via pubsub), clear the line's accountCode so the\n// <select> visibly resets to \"—\". Without this, the option is\n// gone but the form's bound value still holds the stale code,\n// which (a) leaves the user staring at a blank-looking select and\n// (b) used to slip through to submit before the isPostable guard\n// landed. Belt + suspenders.\nwatch(selectableAccountCodes, (codes) => {\n for (const line of lines.value) {\n if (line.accountCode && !codes.has(line.accountCode)) line.accountCode = \"\";\n }\n});\n</script>\n\n<style scoped>\n/* Hide the WebKit / Firefox spin buttons on amount inputs. The\n step attribute still controls validation; this is purely UI.\n Accounting amount fields don't benefit from a spinner — users\n type the number and the up/down arrows just clutter the row. */\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n appearance: textfield;\n}\n</style>\n","<template>\n <form class=\"flex flex-col gap-3\" data-testid=\"accounting-entry-form\" @submit.prevent=\"onSubmit\">\n <!-- Edit mode mounts inside the row's expanded detail panel,\n which already gives the user enough context (the row above\n shows date / kind / memo / lines). Hide the redundant\n \"Edit journal entry\" title there; the editBanner below\n still surfaces the void-and-replace consequence. -->\n <h3 v-if=\"!isEditing\" class=\"text-base font-semibold\">{{ t(\"pluginAccounting.entryForm.title\") }}</h3>\n <div class=\"flex flex-wrap gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.entryForm.dateLabel\") }}\n <input v-model=\"date\" type=\"date\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-entry-date\" />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 grow min-w-0\">\n {{ t(\"pluginAccounting.entryForm.memoLabel\") }}\n <input v-model=\"memo\" type=\"text\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-entry-memo\" />\n </label>\n </div>\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n <th v-if=\"anyTaxLine\" class=\"text-left py-1 px-2 w-40\">{{ t(\"pluginAccounting.entryForm.taxRegistrationIdLabel\") }}</th>\n <th class=\"py-1 px-2\"></th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(line, idx) in lines\" :key=\"idx\" class=\"border-b border-gray-100\">\n <td class=\"py-1 px-2\">\n <select\n v-model=\"line.accountCode\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm bg-white\"\n :data-testid=\"`accounting-entry-line-account-${idx}`\"\n >\n <option value=\"\">{{ DASH }}</option>\n <option v-for=\"account in selectableAccounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"line.debit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white\"\n :data-testid=\"`accounting-entry-line-debit-${idx}`\"\n @input=\"onDebitInput(line)\"\n />\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"line.credit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white\"\n :data-testid=\"`accounting-entry-line-credit-${idx}`\"\n @input=\"onCreditInput(line)\"\n />\n </td>\n <!-- Tax-registration ID column appears only when at least\n one line picks an input-tax account (14xx — see\n isTaxAccountCode). Within a column-visible row, the\n input itself only renders for the lines that actually\n pick a 14xx account; other lines render an empty cell\n so the row keeps its column alignment. -->\n <td v-if=\"anyTaxLine\" class=\"py-1 px-2\">\n <template v-if=\"isTaxLine(line)\">\n <input\n v-model=\"line.taxRegistrationId\"\n type=\"text\"\n :maxlength=\"MAX_TAX_REGISTRATION_ID_LENGTH\"\n :placeholder=\"t('pluginAccounting.entryForm.taxRegistrationIdPlaceholder')\"\n :class=\"[\n 'h-8 px-2 w-full rounded border text-sm font-mono bg-white focus:outline-none',\n isTaxRegistrationIdInvalid(line)\n ? 'border-red-500 ring-1 ring-red-500'\n : isTaxRegistrationIdMissing(line)\n ? 'border-amber-500 ring-1 ring-amber-500'\n : 'border-gray-300 focus:ring-1 focus:ring-blue-500',\n ]\"\n :data-testid=\"`accounting-entry-line-tax-registration-id-${idx}`\"\n :aria-describedby=\"isTaxRegistrationIdMissing(line) ? `accounting-entry-line-tax-registration-id-warning-${idx}` : undefined\"\n />\n <!-- Non-color cue for the amber border. Polite live\n region so screen readers are nudged when the\n user finishes typing an amount and the warning\n first appears, without interrupting other speech. -->\n <p\n v-if=\"isTaxRegistrationIdMissing(line)\"\n :id=\"`accounting-entry-line-tax-registration-id-warning-${idx}`\"\n class=\"text-xs text-amber-600 mt-1\"\n role=\"status\"\n aria-live=\"polite\"\n :data-testid=\"`accounting-entry-line-tax-registration-id-warning-${idx}`\"\n >\n {{ t(\"pluginAccounting.entryForm.taxRegistrationIdMissingWarning\") }}\n </p>\n </template>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <button v-if=\"lines.length > 2\" type=\"button\" class=\"text-xs text-red-500 hover:underline\" @click=\"lines.splice(idx, 1)\">\n {{ t(\"pluginAccounting.entryForm.removeLine\") }}\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n <div class=\"flex items-center justify-between\">\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-entry-add-line\"\n @click=\"addLine\"\n >\n <span class=\"material-icons text-base\">add</span>\n <span>{{ t(\"pluginAccounting.entryForm.addLine\") }}</span>\n </button>\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-entry-manage-accounts\"\n @click=\"showAccountsModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <span :class=\"balanced ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-entry-balance\">\n {{ balanced ? t(\"pluginAccounting.entryForm.balanced\") : t(\"pluginAccounting.entryForm.imbalance\", { amount: imbalanceText }) }}\n </span>\n </div>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-entry-error\">{{ error }}</p>\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-entry-success\">{{ successMessage }}</p>\n <div class=\"flex items-center justify-between gap-2\">\n <!-- editBanner sits on the action row in edit mode (instead\n of as a separate paragraph above the form) so the panel\n is shorter and the user reads the void-and-replace\n consequence right next to the button that triggers it. -->\n <p v-if=\"isEditing\" class=\"text-xs text-gray-500 flex-1 min-w-0\" data-testid=\"accounting-entry-edit-banner\">\n {{ t(\"pluginAccounting.entryForm.editBanner\") }}\n </p>\n <span v-else></span>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-3 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n :disabled=\"submitting\"\n data-testid=\"accounting-entry-cancel-edit\"\n @click=\"emit('cancel')\"\n >\n {{ t(\"pluginAccounting.common.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"!balanced || submitting || editLocked\"\n data-testid=\"accounting-entry-submit\"\n >\n {{ submitButtonLabel }}\n </button>\n </div>\n </div>\n </form>\n <!-- Sibling of the parent <form> on purpose: the modal renders\n its own <form @submit.prevent> for the inline editor, and\n nesting <form>s is invalid HTML that breaks Enter-key submit\n routing in some browsers. Vue 3 multi-root templates let us\n keep the markup flat with no wrapper div. -->\n <AccountsModal v-if=\"showAccountsModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showAccountsModal = false\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { addEntries, voidEntry, type Account, type JournalEntry, type JournalLine } from \"../api\";\nimport { formatAmount, inputStepFor, localDateString, countryHasFeature, type SupportedCountryCode } from \"../../shared\";\nimport { isTaxAccountCode } from \"./accountNumbering\";\nimport AccountsModal from \"./AccountsModal.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[]; currency: string; country?: SupportedCountryCode; entryToEdit?: JournalEntry | null }>();\nconst emit = defineEmits<{ submitted: []; cancel: [] }>();\n\nconst showAccountsModal = ref(false);\n\nconst DASH = \"—\";\n\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n return `${account.name} (${account.code})`;\n}\n\n// Hide deactivated accounts from the entry dropdown — accounting\n// integrity requires keeping them in the chart of accounts (any\n// historical journal line still references the code), but new\n// entries should not be able to land on a soft-deleted account.\nconst selectableAccounts = computed<Account[]>(() => props.accounts.filter((account) => account.active !== false));\nconst selectableAccountCodes = computed<Set<string>>(() => new Set(selectableAccounts.value.map((account) => account.code)));\n\ninterface FormLine {\n accountCode: string;\n debit: number | null;\n credit: number | null;\n taxRegistrationId: string;\n}\n\n// Mirrors server/accounting/journal.ts MAX_TAX_REGISTRATION_ID_LENGTH.\n// Duplicated rather than imported to keep the front-end bundle from\n// pulling in server modules (the existing client / server type alias\n// pattern in api.ts does the same — both sides own their copy of the\n// shape).\nconst MAX_TAX_REGISTRATION_ID_LENGTH = 32;\n\nfunction blankLine(): FormLine {\n return { accountCode: \"\", debit: null, credit: null, taxRegistrationId: \"\" };\n}\n\nfunction isTaxRegistrationIdInvalid(line: FormLine): boolean {\n return line.taxRegistrationId.trim().length > MAX_TAX_REGISTRATION_ID_LENGTH;\n}\n\nfunction isTaxLine(line: FormLine): boolean {\n return line.accountCode !== \"\" && isTaxAccountCode(line.accountCode);\n}\n\n// Soft warning: a postable tax line in a jurisdiction the role\n// prompt requires a counterparty registration number for (JP, EU,\n// GB, IN, AU, NZ, CA — see COUNTRY_FEATURES.warnMissingTaxRegistrationId)\n// gets an amber border + helper text when the field is blank. The\n// form lets the user post anyway (some suppliers genuinely won't\n// have one), but the silent-strip in `toApiLines` no longer goes\n// unnoticed. `function` declarations hoist, so calling `isPostable`\n// here is fine even though it appears later in the file.\nfunction isTaxRegistrationIdMissing(line: FormLine): boolean {\n if (!isTaxLine(line)) return false;\n if (!isPostable(line)) return false;\n if (!countryHasFeature(\"warnMissingTaxRegistrationId\", props.country)) return false;\n return line.taxRegistrationId.trim() === \"\";\n}\n\nconst date = ref(localDateString());\nconst memo = ref(\"\");\nconst lines = ref<FormLine[]>([blankLine(), blankLine()]);\nconst submitting = ref(false);\nconst error = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\n\nconst isEditing = computed<boolean>(() => Boolean(props.entryToEdit));\nconst submitButtonLabel = computed<string>(() => {\n if (submitting.value) {\n return isEditing.value ? t(\"pluginAccounting.entryForm.updating\") : t(\"pluginAccounting.entryForm.submitting\");\n }\n return isEditing.value ? t(\"pluginAccounting.entryForm.update\") : t(\"pluginAccounting.entryForm.submit\");\n});\n\n// One-shot lock: once the user has clicked Update on an edit, the\n// submit button is dead until they Cancel (or land on a different\n// entry). Edit = void + addEntries as two sequential calls; if the\n// void succeeds and the add fails, a second Submit would try to\n// void an already-voided original. We don't add retry plumbing\n// for that — policy is \"report the error, do not retry\". The user\n// cancels out and re-enters manually.\nconst editAttempted = ref(false);\nconst editLocked = computed(() => isEditing.value && editAttempted.value);\n\nfunction addLine(): void {\n lines.value.push(blankLine());\n}\n\n// Toggling ensures a single line never has both sides set — the\n// server validates this too, but doing it on input prevents a\n// confusing UX where the running total goes negative as the user\n// types.\nfunction onDebitInput(line: FormLine): void {\n if (line.debit !== null && line.debit !== 0) line.credit = null;\n}\nfunction onCreditInput(line: FormLine): void {\n if (line.credit !== null && line.credit !== 0) line.debit = null;\n}\n\n// Imbalance is computed off lines that are *postable* (have an\n// accountCode + a positive amount). Without that filter,\n// `balanced` could be `true` even when `toApiLines()` would drop a\n// row, and the user would hit a confusing \"needs ≥ 2 lines\" error\n// from the server on submit.\nconst imbalance = computed<number>(() => {\n let sum = 0;\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n if (isPositiveAmount(line.debit)) sum += line.debit;\n if (isPositiveAmount(line.credit)) sum -= line.credit;\n }\n return sum;\n});\nconst hasAtLeastTwoPostableLines = computed(() => {\n let count = 0;\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n count += 1;\n if (count >= 2) return true;\n }\n return false;\n});\n// Show the tax-registration ID column only when at least one line\n// targets a 14xx (input-tax) account; otherwise the column is\n// wasted space for the typical entry that has no input-tax line.\nconst anyTaxLine = computed(() => lines.value.some(isTaxLine));\nconst hasTaxRegistrationIdError = computed(() => lines.value.some(isTaxRegistrationIdInvalid));\nconst balanced = computed(() => Math.abs(imbalance.value) <= 0.005 && hasAtLeastTwoPostableLines.value && !hasTaxRegistrationIdError.value);\nconst imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));\nconst step = computed(() => inputStepFor(props.currency));\n\nfunction isPositiveAmount(value: unknown): value is number {\n // Robust against the empty string `v-model.number` produces when\n // the user clears a previously-typed field — `\"\" ?? 0 === 0` is\n // false so a naive truthy check would let the empty input through\n // as a phantom zero amount.\n return typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\nfunction isPostable(line: FormLine): boolean {\n if (!line.accountCode) return false;\n // Defence-in-depth against a code that was selectable when the\n // user picked it but got deactivated mid-edit. Hiding the\n // option from the dropdown alone isn't enough — the form's\n // `accountCode` value is sticky, so a stale selection would\n // still be POSTed if the user just hits submit. Gating\n // postability here also flows through to `balanced` and\n // `hasAtLeastTwoPostableLines`, so the submit button disables\n // and the user gets immediate feedback.\n if (!selectableAccountCodes.value.has(line.accountCode)) return false;\n return isPositiveAmount(line.debit) || isPositiveAmount(line.credit);\n}\n\nfunction toApiLines(): JournalLine[] {\n const out: JournalLine[] = [];\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n const apiLine: JournalLine = { accountCode: line.accountCode };\n if (isPositiveAmount(line.debit)) apiLine.debit = line.debit;\n if (isPositiveAmount(line.credit)) apiLine.credit = line.credit;\n // Only persist taxRegistrationId on tax-related lines —\n // otherwise a value typed against an earlier account choice\n // would leak through after the user switched the line to a\n // non-tax account (the input field disappears but the form\n // state lingers).\n if (isTaxLine(line)) {\n const trimmedTaxId = line.taxRegistrationId.trim();\n if (trimmedTaxId !== \"\") apiLine.taxRegistrationId = trimmedTaxId;\n }\n out.push(apiLine);\n }\n return out;\n}\n\nasync function onSubmit(): Promise<void> {\n if (submitting.value || !balanced.value || editLocked.value) return;\n submitting.value = true;\n error.value = null;\n successMessage.value = null;\n try {\n // Edit flow: void the original, then post the replacement.\n // Two sequential calls — not atomic, no retry. Marking\n // `editAttempted` *before* the void disables the submit button\n // for the rest of this edit session (the `editLocked` guard\n // and the button's :disabled both honour it), so a partial\n // failure can't trigger a second void on the already-voided\n // original. On any error: show the message, user must Cancel\n // and re-enter manually.\n const editingId = props.entryToEdit?.id;\n if (editingId) {\n editAttempted.value = true;\n const voidResult = await voidEntry({\n bookId: props.bookId,\n entryId: editingId,\n reason: t(\"pluginAccounting.entryForm.editVoidReason\"),\n });\n if (!voidResult.ok) {\n error.value = voidResult.error;\n return;\n }\n }\n const result = await addEntries({\n bookId: props.bookId,\n entries: [\n {\n date: date.value,\n memo: memo.value.trim() || undefined,\n lines: toApiLines(),\n ...(editingId ? { replacesEntryId: editingId } : {}),\n },\n ],\n });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n successMessage.value = editingId ? t(\"pluginAccounting.entryForm.editSuccess\") : t(\"pluginAccounting.entryForm.success\");\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n emit(\"submitted\");\n } catch (err) {\n // apiPost normally folds network / HTTP failures into\n // `result.ok = false`, so this branch should be rare. It's a\n // belt-and-braces guard against a runtime failure leaving the\n // submit button stuck — the user gets a visible error\n // instead of an unhandled rejection.\n error.value = errorMessage(err);\n } finally {\n submitting.value = false;\n }\n}\n\n// Reset the entire draft when bookId switches under us (rare but\n// possible via BookSwitcher while the form is open). Carrying the\n// previous book's lines and account codes into the new book is\n// the worst kind of silent failure — the new book might not even\n// have the same chart of accounts.\nwatch(\n () => props.bookId,\n () => {\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n date.value = localDateString();\n error.value = null;\n successMessage.value = null;\n },\n);\n\n// Edit mode: when the parent hands us an entry to edit, pre-fill\n// every field so the user can tweak and resubmit. When it clears\n// the prop (after submit / cancel / book switch), wipe back to a\n// blank draft so the next \"New entry\" tab visit is fresh. Mapping\n// `entry.lines` (the wire shape with optional `debit` / `credit`)\n// onto `FormLine` (which uses nullable numbers + a string\n// taxRegistrationId) is straightforward — pad missing optionals\n// to null / \"\".\nwatch(\n () => props.entryToEdit,\n (entry) => {\n error.value = null;\n successMessage.value = null;\n // Fresh edit (or exit from edit mode) → unlock the submit\n // button so the new edit session has a clean shot.\n editAttempted.value = false;\n if (!entry) {\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n date.value = localDateString();\n return;\n }\n date.value = entry.date;\n memo.value = entry.memo ?? \"\";\n lines.value = entry.lines.map((line) => ({\n accountCode: line.accountCode,\n debit: typeof line.debit === \"number\" ? line.debit : null,\n credit: typeof line.credit === \"number\" ? line.credit : null,\n taxRegistrationId: line.taxRegistrationId ?? \"\",\n }));\n if (lines.value.length < 2) {\n while (lines.value.length < 2) lines.value.push(blankLine());\n }\n },\n { immediate: true },\n);\n\n// If an account the user already picked gets deactivated mid-edit\n// (e.g. via the Manage Accounts modal in this form, or from\n// another tab via pubsub), clear the line's accountCode so the\n// <select> visibly resets to \"—\". Without this, the option is\n// gone but the form's bound value still holds the stale code,\n// which (a) leaves the user staring at a blank-looking select and\n// (b) used to slip through to submit before the isPostable guard\n// landed. Belt + suspenders.\nwatch(selectableAccountCodes, (codes) => {\n for (const line of lines.value) {\n if (line.accountCode && !codes.has(line.accountCode)) line.accountCode = \"\";\n }\n});\n</script>\n\n<style scoped>\n/* Hide the WebKit / Firefox spin buttons on amount inputs. The\n step attribute still controls validation; this is purely UI.\n Accounting amount fields don't benefit from a spinner — users\n type the number and the up/down arrows just clutter the row. */\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n appearance: textfield;\n}\n</style>\n","<template>\n <div class=\"flex flex-col h-full gap-3\">\n <!-- Top-row toolbar slot. Renders the embedded entry form\n in \"+ New entry\" mode here; Edit-mode for a row's existing\n entry is rendered IN-PLACE inside that row's expanded\n detail panel below. The date picker / account filter /\n table below stay visible in either state. -->\n <div v-if=\"showNewForm\" class=\"border border-gray-200 rounded p-3\" data-testid=\"accounting-journal-inline-form\">\n <JournalEntryForm\n :book-id=\"bookId\"\n :accounts=\"accounts\"\n :currency=\"currency\"\n :country=\"country\"\n :entry-to-edit=\"null\"\n @submitted=\"onFormSubmitted\"\n @cancel=\"onFormCancel\"\n />\n </div>\n <div v-else class=\"flex items-center justify-end\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-journal-new-entry\"\n @click=\"onOpenNewEntry\"\n >\n <span class=\"material-icons text-base\">add</span>\n <span>{{ t(\"pluginAccounting.tabs.newEntry\") }}</span>\n </button>\n </div>\n <div class=\"flex flex-wrap items-end gap-2\">\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.journalList.accountLabel\") }}\n <select v-model=\"accountCode\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-journal-account\">\n <option value=\"\">{{ t(\"pluginAccounting.journalList.allAccounts\") }}</option>\n <option v-for=\"account in accounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </label>\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <!-- Scrollable list area: only the entries list scrolls below\n this point. The new-entry slot + filter bar above stay\n pinned by virtue of NOT being inside this scroll container,\n and the column-header row stays visible via `position:\n sticky` on its <th>s. `min-h-0` is required for the flex-1\n child to actually shrink below its content height in a\n flex-col parent. -->\n <div class=\"flex-1 min-h-0 overflow-auto\">\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <p v-else-if=\"visibleEntries.length === 0\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.empty\") }}</p>\n <table v-else class=\"w-full text-sm\" data-testid=\"accounting-journal-table\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <!-- Per-<th> sticky (rather than `<thead class=\"sticky\">`)\n for compatibility — `position: sticky` on the\n table-header-group display is brittle in some\n browsers, but on `<th>` it's universally supported.\n `bg-white` is required so the scrolled rows beneath\n don't bleed through. -->\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.date\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.kind\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.memo\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.lines\") }}</th>\n </tr>\n </thead>\n <tbody>\n <template v-for=\"entry in visibleEntries\" :key=\"entry.id\">\n <tr\n :class=\"[\n voidedEntryIds.has(entry.id) ? 'text-gray-400 line-through' : '',\n expandedEntryId === entry.id ? 'row-selected' : '',\n 'border-b border-gray-100 align-top cursor-pointer hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400',\n ]\"\n :data-testid=\"voidedEntryIds.has(entry.id) ? `accounting-journal-row-voided-${entry.id}` : `accounting-journal-row-${entry.id}`\"\n tabindex=\"0\"\n role=\"button\"\n :aria-expanded=\"expandedEntryId === entry.id\"\n @click=\"toggleExpanded(entry.id)\"\n @keydown.enter.prevent.self=\"onKeyToggle($event, entry.id)\"\n @keydown.space.prevent.self=\"onKeyToggle($event, entry.id)\"\n >\n <td class=\"py-1 px-2 whitespace-nowrap\">{{ entry.date }}</td>\n <td class=\"py-1 px-2 text-xs\">{{ kindLabel(entry.kind) }}</td>\n <td class=\"py-1 px-2\">\n <span v-if=\"entry.memo\">{{ entry.memo }}</span>\n </td>\n <td class=\"py-1 px-2\">\n <template v-if=\"expandedEntryId !== entry.id\">\n <div v-for=\"(line, idx) in entry.lines\" :key=\"idx\" class=\"text-xs flex gap-2 items-baseline\">\n <span class=\"font-mono text-[10px] text-gray-400\">{{ line.accountCode }}</span>\n <span v-if=\"accountNameFor(line.accountCode)\">{{ accountNameFor(line.accountCode) }}</span>\n <span v-if=\"line.debit\">{{ formatDebit(line.debit) }}</span>\n <span v-if=\"line.credit\">{{ formatCredit(line.credit) }}</span>\n </div>\n </template>\n <div v-else class=\"flex items-center justify-between gap-2\">\n <span class=\"text-xs text-gray-400 font-mono\">{{ formatCreatedAt(entry.createdAt) }}</span>\n <button\n type=\"button\"\n class=\"h-6 w-6 flex items-center justify-center rounded text-gray-500 hover:bg-gray-100\"\n :data-testid=\"`accounting-journal-detail-close-${entry.id}`\"\n :aria-label=\"t('common.close')\"\n @click.stop=\"onCloseDetail\"\n >\n <span class=\"material-icons text-base\">close</span>\n </button>\n </div>\n </td>\n </tr>\n <tr v-if=\"expandedEntryId === entry.id\" class=\"bg-gray-50 detail-selected\" :data-testid=\"`accounting-journal-detail-${entry.id}`\">\n <td :colspan=\"4\" class=\"px-6 py-2\">\n <!-- Edit-in-place: the JournalEntryForm replaces the\n read-only detail content for this row when the\n user clicks Edit. Submit / cancel collapses back\n (submit also voids the original, so we clear the\n selection); top-bar \"+ New entry\" stays a separate\n path that opens the same form above the table. -->\n <div v-if=\"entryBeingEdited?.id === entry.id\" :data-testid=\"`accounting-journal-detail-edit-${entry.id}`\">\n <JournalEntryForm\n :book-id=\"bookId\"\n :accounts=\"accounts\"\n :currency=\"currency\"\n :country=\"country\"\n :entry-to-edit=\"entryBeingEdited\"\n @submitted=\"onFormSubmitted\"\n @cancel=\"onFormCancel\"\n />\n </div>\n <template v-else>\n <div class=\"flex items-center gap-3 mb-2\">\n <template v-if=\"entry.kind === 'normal' && !voidedEntryIds.has(entry.id)\">\n <button class=\"text-xs text-blue-600 hover:underline\" :data-testid=\"`accounting-edit-${entry.id}`\" @click=\"onEditEntry(entry)\">\n {{ t(\"pluginAccounting.journalList.edit\") }}\n </button>\n <button class=\"text-xs text-red-500 hover:underline\" :data-testid=\"`accounting-void-${entry.id}`\" @click=\"onVoid(entry)\">\n {{ t(\"pluginAccounting.journalList.void\") }}\n </button>\n </template>\n <button\n v-else-if=\"entry.kind === 'opening' && !voidedEntryIds.has(entry.id)\"\n class=\"text-xs text-blue-600 hover:underline\"\n :data-testid=\"`accounting-edit-opening-${entry.id}`\"\n @click=\"emit('editOpening')\"\n >\n {{ t(\"pluginAccounting.journalList.edit\") }}\n </button>\n </div>\n <table class=\"w-full text-xs\">\n <thead>\n <tr class=\"text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.memoLabel\") }}</th>\n <th v-if=\"entryHasTaxIds(entry)\" class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.taxRegistrationIdLabel\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(line, idx) in entry.lines\" :key=\"idx\" class=\"border-b border-gray-100 text-gray-700\">\n <td class=\"py-1 px-2\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ line.accountCode }}</span>\n <span v-if=\"accountNameFor(line.accountCode)\">{{ accountNameFor(line.accountCode) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">\n <template v-if=\"line.debit\">{{ formatAmount(line.debit, currency) }}</template>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">\n <template v-if=\"line.credit\">{{ formatAmount(line.credit, currency) }}</template>\n </td>\n <td class=\"py-1 px-2\">\n <template v-if=\"line.memo\">{{ line.memo }}</template>\n </td>\n <td v-if=\"entryHasTaxIds(entry)\" class=\"py-1 px-2 font-mono text-[10px]\">\n <template v-if=\"line.taxRegistrationId\">{{ line.taxRegistrationId }}</template>\n </td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300 text-gray-700\">\n <td class=\"py-1 px-2 text-gray-500\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(entryDebitTotal(entry), currency) }}</td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(entryCreditTotal(entry), currency) }}</td>\n <td :colspan=\"entryHasTaxIds(entry) ? 2 : 1\"></td>\n </tr>\n </tfoot>\n </table>\n </template>\n </td>\n </tr>\n </template>\n </tbody>\n </table>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { getJournalEntries, voidEntry, type Account, type JournalEntry, type JournalEntryKind, type JournalLine } from \"../api\";\nimport { formatAmount, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd, type SupportedCountryCode } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\nimport JournalEntryForm from \"./JournalEntryForm.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n bookId: string;\n accounts: Account[];\n currency: string;\n country?: SupportedCountryCode;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n /** Entry id to auto-expand and scroll into view. Surfaced by the\n * parent when an `addEntries` tool result lands so the user sees\n * the freshly-posted row highlighted. Captured into\n * `pendingPreselectId` and consumed once the entry actually\n * appears in the fetched list — refetch can race the prop. */\n preselectEntryId?: string;\n}>();\nconst emit = defineEmits<{ editOpening: []; preselectConsumed: [] }>();\n\n// Inline-form state. Two distinct surfaces, one component:\n// • showNewForm = true → blank draft, rendered above the table\n// where the \"+ New entry\" button used to be.\n// • entryBeingEdited != null → edit mode, rendered IN-PLACE inside\n// the matching row's expanded detail panel (replacing the read-\n// only debit/credit table for that row).\n// `<JournalEntryForm>` looks at `entryToEdit` to decide its title /\n// submit label; the top-bar instance always passes null.\nconst showNewForm = ref(false);\nconst entryBeingEdited = ref<JournalEntry | null>(null);\n// Single-selection detail expansion. Clicking a row swaps the\n// selection (or collapses if it's already the selected row).\n// Cleared on book switch via the closeForm watcher; entries deleted\n// between fetches simply drop out of filteredEntries, so a stale id\n// here just renders no detail row. Declared early so the\n// onFormSubmitted / bookId-watcher callbacks below can reference it.\nconst expandedEntryId = ref<string | null>(null);\n\nfunction onOpenNewEntry(): void {\n entryBeingEdited.value = null;\n showNewForm.value = true;\n}\n\nfunction onEditEntry(entry: JournalEntry): void {\n showNewForm.value = false;\n entryBeingEdited.value = entry;\n}\n\nfunction closeForm(): void {\n showNewForm.value = false;\n entryBeingEdited.value = null;\n}\n\nfunction onFormSubmitted(): void {\n // Submit posts via the form. In production the server-side\n // publishBookChange round-trips an SSE event that bumps\n // `bookVersion` and re-runs `refresh` via the watcher below.\n // We also kick a synchronous refetch here so the freshly-posted\n // row shows up immediately — the SSE round-trip can race the\n // tab repaint, and skipping it here also makes the e2e mock\n // path (no pubsub replay) deterministic.\n closeForm();\n // After an in-place edit submit, the original entry is voided\n // and replaced. Collapse the detail panel since it was pointing\n // at an entry that's now superseded.\n expandedEntryId.value = null;\n void refresh();\n}\n\nfunction onFormCancel(): void {\n closeForm();\n}\n\n// Switching books mid-edit would carry the prior book's draft into\n// the new book. Force the panel closed so the next visit starts\n// from a blank toolbar — the form's own bookId watcher would also\n// reset its internal state, but we want the user back in the\n// neutral \"+ New entry\" surface.\nwatch(\n () => props.bookId,\n () => {\n closeForm();\n expandedEntryId.value = null;\n },\n);\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\n// Default = current fiscal year. Reset by the bookId/fiscalYearEnd\n// watcher so switching books or changing the FY-end in settings\n// drops a stale custom range from the prior book.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\nconst accountCode = ref(\"\");\nconst entries = ref<JournalEntry[]>([]);\nconst serverVoidedIds = ref<string[]>([]);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction kindLabel(kind: JournalEntryKind): string {\n if (kind === \"opening\") return t(\"pluginAccounting.journalList.kind.opening\");\n if (kind === \"void\") return t(\"pluginAccounting.journalList.kind.void\");\n if (kind === \"void-marker\") return t(\"pluginAccounting.journalList.kind.voidMarker\");\n return t(\"pluginAccounting.journalList.kind.normal\");\n}\n\nfunction formatDebit(value: number): string {\n return `DR ${formatAmount(value, props.currency)}`;\n}\nfunction formatCredit(value: number): string {\n return `CR ${formatAmount(value, props.currency)}`;\n}\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n // Same convention used by JournalEntryForm and Ledger pickers.\n return `${account.name} (${account.code})`;\n}\n// `entry.createdAt` is server-stamped ISO 8601. We render local\n// date+time (no seconds, no timezone) in YYYY-MM-DD HH:MM form to\n// match `entry.date`'s style and keep the line compact. Parens are\n// baked in here so the template doesn't carry raw text (the\n// vue-i18n/no-raw-text rule flags literal strings in mustache).\nfunction formatCreatedAt(iso: string): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return `(${iso})`;\n const pad = (num: number): string => String(num).padStart(2, \"0\");\n return `(${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())})`;\n}\nconst accountNameByCode = computed(() => {\n const map = new Map<string, string>();\n for (const account of props.accounts) map.set(account.code, account.name);\n return map;\n});\nfunction accountNameFor(code: string): string | null {\n return accountNameByCode.value.get(code) ?? null;\n}\n\n// Close button on the selected row's lines cell. Has to clear BOTH\n// expandedEntryId AND entryBeingEdited — if the user clicks Edit\n// (which sets entryBeingEdited) and then clicks Close, leaving\n// entryBeingEdited stale would block reopening: toggleExpanded's\n// edit-mode guard early-returns when entryBeingEdited.id matches the\n// clicked row, so the user could never reopen that entry from the\n// list. Issue surfaced by the CodeRabbit review on PR #1161.\nfunction onCloseDetail(): void {\n expandedEntryId.value = null;\n entryBeingEdited.value = null;\n}\n\nfunction toggleExpanded(entryId: string): void {\n // While the row is in edit mode for itself, ignore clicks on the\n // row chrome (date / kind / memo / lines cells) — the user is\n // actively typing into the form below and a stray cell click\n // shouldn't collapse the panel. Cancel / Submit on the form, or\n // clicking a different row, are the deliberate exits.\n if (entryBeingEdited.value?.id === entryId) return;\n expandedEntryId.value = expandedEntryId.value === entryId ? null : entryId;\n // Switching to a different row (or collapsing) drops any\n // in-progress edit on the prior row.\n entryBeingEdited.value = null;\n}\n\nfunction onKeyToggle(event: KeyboardEvent, entryId: string): void {\n if (event.repeat) return;\n toggleExpanded(entryId);\n}\n\nfunction entryHasTaxIds(entry: JournalEntry): boolean {\n return entry.lines.some((line) => Boolean(line.taxRegistrationId));\n}\n\nfunction sumLines(lines: JournalLine[], pick: (line: JournalLine) => number | undefined): number {\n return lines.reduce((acc, line) => acc + (pick(line) ?? 0), 0);\n}\n\nfunction entryDebitTotal(entry: JournalEntry): number {\n return sumLines(entry.lines, (line) => line.debit);\n}\n\nfunction entryCreditTotal(entry: JournalEntry): number {\n return sumLines(entry.lines, (line) => line.credit);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n const result = await getJournalEntries({\n bookId: props.bookId,\n from: range.value.from || undefined,\n to: range.value.to || undefined,\n accountCode: accountCode.value || undefined,\n });\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n entries.value = [];\n serverVoidedIds.value = [];\n return;\n }\n entries.value = result.data.entries;\n serverVoidedIds.value = result.data.voidedEntryIds;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nconst filteredEntries = computed(() => entries.value);\n\n// Visible-list view that pins the entry currently being edited at\n// the top when a filter change or pubsub-driven refetch would\n// otherwise drop it from `filteredEntries`. Without this, the\n// in-place edit form (which is nested under the row's v-if /\n// v-for) would unmount and silently discard the user's draft when:\n// • the user adjusts the date range or account filter,\n// • a sibling tab / LLM tool voids the entry out-of-band and the\n// SSE pubsub bumps `bookVersion`, refetching this list,\n// • or a sibling tab / LLM tool deletes the underlying book.\n// Pinning the editing entry from the local snapshot (entryBeingEdited)\n// keeps the form mounted across all three. The pinned row sits at\n// the top of the table while editing; on submit / cancel the\n// snapshot clears and the list reverts to filteredEntries.\nconst visibleEntries = computed<JournalEntry[]>(() => {\n const list = filteredEntries.value;\n const editing = entryBeingEdited.value;\n if (editing && !list.some((entry) => entry.id === editing.id)) {\n return [editing, ...list];\n }\n return list;\n});\n\n// Set of original entry ids that have been voided. The server\n// computes this from the *unfiltered* journal (so an account-filtered\n// query — which drops void-marker rows because they have no lines —\n// still strikes out the cancelled original). Source of truth on the\n// server is `voidedIdSet()` in journal.ts.\nconst voidedEntryIds = computed(() => new Set(serverVoidedIds.value));\n\nasync function onVoid(entry: JournalEntry): Promise<void> {\n // Single dialog: the prompt is the confirmation. Cancelling\n // (returning null) cancels the void; entering empty text or a\n // reason proceeds.\n const reason = window.prompt(t(\"pluginAccounting.journalList.voidReason\"));\n if (reason === null) return;\n try {\n const result = await voidEntry({ entryId: entry.id, reason: reason || undefined, bookId: props.bookId });\n if (!result.ok) error.value = result.error;\n } catch (err) {\n error.value = errorMessage(err);\n }\n}\n\n// Reset to current-year window whenever the active book or its\n// fiscal-year end changes. Keeps a custom range from leaking across\n// books and follows a settings-driven shift in fiscalYearEnd.\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\nwatch(() => [props.bookId, props.version, range.value.from, range.value.to, accountCode.value], refresh, { immediate: true });\n\n// Pending preselect: the parent hands us an id via `preselectEntryId`,\n// but the matching entry may not be in `entries` yet (the SSE-driven\n// refetch lands on its own clock). Stash it here, then the\n// [pendingPreselectId, entries] watcher below consumes it once the\n// row actually exists in the list — and clears it so subsequent\n// unrelated refetches (void events, sibling-tab edits) don't\n// re-expand a stale target.\nconst pendingPreselectId = ref<string | null>(null);\n\nwatch(\n () => props.preselectEntryId,\n (incoming) => {\n if (incoming) pendingPreselectId.value = incoming;\n },\n // immediate: true so a late JournalList mount (the View defers our\n // mount until refetchBooks resolves activeBookId) still captures\n // a preselect the parent had already set — without this, a normal\n // watcher misses the \"initial value is the target value\" case.\n { immediate: true },\n);\n\nwatch([pendingPreselectId, entries], async ([targetId, list]) => {\n if (!targetId) return;\n if (!list.some((entry) => entry.id === targetId)) return;\n // Always emit `preselectConsumed` (whether we expand or bail) so\n // the parent can drop its `journalPreselectEntryId` ref. Without\n // this one-shot signal, leaving and returning to the journal tab\n // (v-if remount) replays the immediate prop watcher against the\n // stale value, re-expanding an old row the user has already moved\n // past. Issue raised by the Codex automated review on PR #1158.\n if (entryBeingEdited.value) {\n // Don't overwrite an in-progress edit on another row — the\n // user's draft matters more than the highlight. Drop pending so\n // we don't keep retrying every refetch, and signal consumed so\n // the parent doesn't keep re-handing us the same id.\n pendingPreselectId.value = null;\n emit(\"preselectConsumed\");\n return;\n }\n expandedEntryId.value = targetId;\n await nextTick();\n const row =\n document.querySelector(`[data-testid=\"accounting-journal-row-${targetId}\"]`) ??\n document.querySelector(`[data-testid=\"accounting-journal-row-voided-${targetId}\"]`);\n row?.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n pendingPreselectId.value = null;\n emit(\"preselectConsumed\");\n});\n</script>\n\n<style scoped>\n/* Selection frame for the expanded entry. Borders go on the cells\n (not the <tr>) because border-collapse: collapse — Tailwind's\n default — eats <tr>-level borders/box-shadows. The entry row owns\n top/left/right; the detail-panel row directly below owns\n left/right/bottom, so together they read as one rectangle around\n the selection. Color matches the focus-ring blue used elsewhere\n in this list. */\n.row-selected > td {\n background-color: rgb(239 246 255); /* tailwind blue-50 */\n border-top: 2px solid rgb(59 130 246); /* tailwind blue-500 */\n}\n.row-selected > td:first-child {\n border-left: 2px solid rgb(59 130 246);\n}\n.row-selected > td:last-child {\n border-right: 2px solid rgb(59 130 246);\n}\n.detail-selected > td {\n background-color: rgb(239 246 255);\n border-left: 2px solid rgb(59 130 246);\n border-right: 2px solid rgb(59 130 246);\n border-bottom: 2px solid rgb(59 130 246);\n}\n</style>\n","<template>\n <div class=\"flex flex-col h-full gap-3\">\n <!-- Top-row toolbar slot. Renders the embedded entry form\n in \"+ New entry\" mode here; Edit-mode for a row's existing\n entry is rendered IN-PLACE inside that row's expanded\n detail panel below. The date picker / account filter /\n table below stay visible in either state. -->\n <div v-if=\"showNewForm\" class=\"border border-gray-200 rounded p-3\" data-testid=\"accounting-journal-inline-form\">\n <JournalEntryForm\n :book-id=\"bookId\"\n :accounts=\"accounts\"\n :currency=\"currency\"\n :country=\"country\"\n :entry-to-edit=\"null\"\n @submitted=\"onFormSubmitted\"\n @cancel=\"onFormCancel\"\n />\n </div>\n <div v-else class=\"flex items-center justify-end\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-journal-new-entry\"\n @click=\"onOpenNewEntry\"\n >\n <span class=\"material-icons text-base\">add</span>\n <span>{{ t(\"pluginAccounting.tabs.newEntry\") }}</span>\n </button>\n </div>\n <div class=\"flex flex-wrap items-end gap-2\">\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.journalList.accountLabel\") }}\n <select v-model=\"accountCode\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-journal-account\">\n <option value=\"\">{{ t(\"pluginAccounting.journalList.allAccounts\") }}</option>\n <option v-for=\"account in accounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </label>\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <!-- Scrollable list area: only the entries list scrolls below\n this point. The new-entry slot + filter bar above stay\n pinned by virtue of NOT being inside this scroll container,\n and the column-header row stays visible via `position:\n sticky` on its <th>s. `min-h-0` is required for the flex-1\n child to actually shrink below its content height in a\n flex-col parent. -->\n <div class=\"flex-1 min-h-0 overflow-auto\">\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <p v-else-if=\"visibleEntries.length === 0\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.empty\") }}</p>\n <table v-else class=\"w-full text-sm\" data-testid=\"accounting-journal-table\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <!-- Per-<th> sticky (rather than `<thead class=\"sticky\">`)\n for compatibility — `position: sticky` on the\n table-header-group display is brittle in some\n browsers, but on `<th>` it's universally supported.\n `bg-white` is required so the scrolled rows beneath\n don't bleed through. -->\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.date\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.kind\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.memo\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.lines\") }}</th>\n </tr>\n </thead>\n <tbody>\n <template v-for=\"entry in visibleEntries\" :key=\"entry.id\">\n <tr\n :class=\"[\n voidedEntryIds.has(entry.id) ? 'text-gray-400 line-through' : '',\n expandedEntryId === entry.id ? 'row-selected' : '',\n 'border-b border-gray-100 align-top cursor-pointer hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400',\n ]\"\n :data-testid=\"voidedEntryIds.has(entry.id) ? `accounting-journal-row-voided-${entry.id}` : `accounting-journal-row-${entry.id}`\"\n tabindex=\"0\"\n role=\"button\"\n :aria-expanded=\"expandedEntryId === entry.id\"\n @click=\"toggleExpanded(entry.id)\"\n @keydown.enter.prevent.self=\"onKeyToggle($event, entry.id)\"\n @keydown.space.prevent.self=\"onKeyToggle($event, entry.id)\"\n >\n <td class=\"py-1 px-2 whitespace-nowrap\">{{ entry.date }}</td>\n <td class=\"py-1 px-2 text-xs\">{{ kindLabel(entry.kind) }}</td>\n <td class=\"py-1 px-2\">\n <span v-if=\"entry.memo\">{{ entry.memo }}</span>\n </td>\n <td class=\"py-1 px-2\">\n <template v-if=\"expandedEntryId !== entry.id\">\n <div v-for=\"(line, idx) in entry.lines\" :key=\"idx\" class=\"text-xs flex gap-2 items-baseline\">\n <span class=\"font-mono text-[10px] text-gray-400\">{{ line.accountCode }}</span>\n <span v-if=\"accountNameFor(line.accountCode)\">{{ accountNameFor(line.accountCode) }}</span>\n <span v-if=\"line.debit\">{{ formatDebit(line.debit) }}</span>\n <span v-if=\"line.credit\">{{ formatCredit(line.credit) }}</span>\n </div>\n </template>\n <div v-else class=\"flex items-center justify-between gap-2\">\n <span class=\"text-xs text-gray-400 font-mono\">{{ formatCreatedAt(entry.createdAt) }}</span>\n <button\n type=\"button\"\n class=\"h-6 w-6 flex items-center justify-center rounded text-gray-500 hover:bg-gray-100\"\n :data-testid=\"`accounting-journal-detail-close-${entry.id}`\"\n :aria-label=\"t('common.close')\"\n @click.stop=\"onCloseDetail\"\n >\n <span class=\"material-icons text-base\">close</span>\n </button>\n </div>\n </td>\n </tr>\n <tr v-if=\"expandedEntryId === entry.id\" class=\"bg-gray-50 detail-selected\" :data-testid=\"`accounting-journal-detail-${entry.id}`\">\n <td :colspan=\"4\" class=\"px-6 py-2\">\n <!-- Edit-in-place: the JournalEntryForm replaces the\n read-only detail content for this row when the\n user clicks Edit. Submit / cancel collapses back\n (submit also voids the original, so we clear the\n selection); top-bar \"+ New entry\" stays a separate\n path that opens the same form above the table. -->\n <div v-if=\"entryBeingEdited?.id === entry.id\" :data-testid=\"`accounting-journal-detail-edit-${entry.id}`\">\n <JournalEntryForm\n :book-id=\"bookId\"\n :accounts=\"accounts\"\n :currency=\"currency\"\n :country=\"country\"\n :entry-to-edit=\"entryBeingEdited\"\n @submitted=\"onFormSubmitted\"\n @cancel=\"onFormCancel\"\n />\n </div>\n <template v-else>\n <div class=\"flex items-center gap-3 mb-2\">\n <template v-if=\"entry.kind === 'normal' && !voidedEntryIds.has(entry.id)\">\n <button class=\"text-xs text-blue-600 hover:underline\" :data-testid=\"`accounting-edit-${entry.id}`\" @click=\"onEditEntry(entry)\">\n {{ t(\"pluginAccounting.journalList.edit\") }}\n </button>\n <button class=\"text-xs text-red-500 hover:underline\" :data-testid=\"`accounting-void-${entry.id}`\" @click=\"onVoid(entry)\">\n {{ t(\"pluginAccounting.journalList.void\") }}\n </button>\n </template>\n <button\n v-else-if=\"entry.kind === 'opening' && !voidedEntryIds.has(entry.id)\"\n class=\"text-xs text-blue-600 hover:underline\"\n :data-testid=\"`accounting-edit-opening-${entry.id}`\"\n @click=\"emit('editOpening')\"\n >\n {{ t(\"pluginAccounting.journalList.edit\") }}\n </button>\n </div>\n <table class=\"w-full text-xs\">\n <thead>\n <tr class=\"text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.memoLabel\") }}</th>\n <th v-if=\"entryHasTaxIds(entry)\" class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.taxRegistrationIdLabel\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(line, idx) in entry.lines\" :key=\"idx\" class=\"border-b border-gray-100 text-gray-700\">\n <td class=\"py-1 px-2\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ line.accountCode }}</span>\n <span v-if=\"accountNameFor(line.accountCode)\">{{ accountNameFor(line.accountCode) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">\n <template v-if=\"line.debit\">{{ formatAmount(line.debit, currency) }}</template>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">\n <template v-if=\"line.credit\">{{ formatAmount(line.credit, currency) }}</template>\n </td>\n <td class=\"py-1 px-2\">\n <template v-if=\"line.memo\">{{ line.memo }}</template>\n </td>\n <td v-if=\"entryHasTaxIds(entry)\" class=\"py-1 px-2 font-mono text-[10px]\">\n <template v-if=\"line.taxRegistrationId\">{{ line.taxRegistrationId }}</template>\n </td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300 text-gray-700\">\n <td class=\"py-1 px-2 text-gray-500\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(entryDebitTotal(entry), currency) }}</td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(entryCreditTotal(entry), currency) }}</td>\n <td :colspan=\"entryHasTaxIds(entry) ? 2 : 1\"></td>\n </tr>\n </tfoot>\n </table>\n </template>\n </td>\n </tr>\n </template>\n </tbody>\n </table>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { getJournalEntries, voidEntry, type Account, type JournalEntry, type JournalEntryKind, type JournalLine } from \"../api\";\nimport { formatAmount, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd, type SupportedCountryCode } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\nimport JournalEntryForm from \"./JournalEntryForm.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n bookId: string;\n accounts: Account[];\n currency: string;\n country?: SupportedCountryCode;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n /** Entry id to auto-expand and scroll into view. Surfaced by the\n * parent when an `addEntries` tool result lands so the user sees\n * the freshly-posted row highlighted. Captured into\n * `pendingPreselectId` and consumed once the entry actually\n * appears in the fetched list — refetch can race the prop. */\n preselectEntryId?: string;\n}>();\nconst emit = defineEmits<{ editOpening: []; preselectConsumed: [] }>();\n\n// Inline-form state. Two distinct surfaces, one component:\n// • showNewForm = true → blank draft, rendered above the table\n// where the \"+ New entry\" button used to be.\n// • entryBeingEdited != null → edit mode, rendered IN-PLACE inside\n// the matching row's expanded detail panel (replacing the read-\n// only debit/credit table for that row).\n// `<JournalEntryForm>` looks at `entryToEdit` to decide its title /\n// submit label; the top-bar instance always passes null.\nconst showNewForm = ref(false);\nconst entryBeingEdited = ref<JournalEntry | null>(null);\n// Single-selection detail expansion. Clicking a row swaps the\n// selection (or collapses if it's already the selected row).\n// Cleared on book switch via the closeForm watcher; entries deleted\n// between fetches simply drop out of filteredEntries, so a stale id\n// here just renders no detail row. Declared early so the\n// onFormSubmitted / bookId-watcher callbacks below can reference it.\nconst expandedEntryId = ref<string | null>(null);\n\nfunction onOpenNewEntry(): void {\n entryBeingEdited.value = null;\n showNewForm.value = true;\n}\n\nfunction onEditEntry(entry: JournalEntry): void {\n showNewForm.value = false;\n entryBeingEdited.value = entry;\n}\n\nfunction closeForm(): void {\n showNewForm.value = false;\n entryBeingEdited.value = null;\n}\n\nfunction onFormSubmitted(): void {\n // Submit posts via the form. In production the server-side\n // publishBookChange round-trips an SSE event that bumps\n // `bookVersion` and re-runs `refresh` via the watcher below.\n // We also kick a synchronous refetch here so the freshly-posted\n // row shows up immediately — the SSE round-trip can race the\n // tab repaint, and skipping it here also makes the e2e mock\n // path (no pubsub replay) deterministic.\n closeForm();\n // After an in-place edit submit, the original entry is voided\n // and replaced. Collapse the detail panel since it was pointing\n // at an entry that's now superseded.\n expandedEntryId.value = null;\n void refresh();\n}\n\nfunction onFormCancel(): void {\n closeForm();\n}\n\n// Switching books mid-edit would carry the prior book's draft into\n// the new book. Force the panel closed so the next visit starts\n// from a blank toolbar — the form's own bookId watcher would also\n// reset its internal state, but we want the user back in the\n// neutral \"+ New entry\" surface.\nwatch(\n () => props.bookId,\n () => {\n closeForm();\n expandedEntryId.value = null;\n },\n);\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\n// Default = current fiscal year. Reset by the bookId/fiscalYearEnd\n// watcher so switching books or changing the FY-end in settings\n// drops a stale custom range from the prior book.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\nconst accountCode = ref(\"\");\nconst entries = ref<JournalEntry[]>([]);\nconst serverVoidedIds = ref<string[]>([]);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction kindLabel(kind: JournalEntryKind): string {\n if (kind === \"opening\") return t(\"pluginAccounting.journalList.kind.opening\");\n if (kind === \"void\") return t(\"pluginAccounting.journalList.kind.void\");\n if (kind === \"void-marker\") return t(\"pluginAccounting.journalList.kind.voidMarker\");\n return t(\"pluginAccounting.journalList.kind.normal\");\n}\n\nfunction formatDebit(value: number): string {\n return `DR ${formatAmount(value, props.currency)}`;\n}\nfunction formatCredit(value: number): string {\n return `CR ${formatAmount(value, props.currency)}`;\n}\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n // Same convention used by JournalEntryForm and Ledger pickers.\n return `${account.name} (${account.code})`;\n}\n// `entry.createdAt` is server-stamped ISO 8601. We render local\n// date+time (no seconds, no timezone) in YYYY-MM-DD HH:MM form to\n// match `entry.date`'s style and keep the line compact. Parens are\n// baked in here so the template doesn't carry raw text (the\n// vue-i18n/no-raw-text rule flags literal strings in mustache).\nfunction formatCreatedAt(iso: string): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return `(${iso})`;\n const pad = (num: number): string => String(num).padStart(2, \"0\");\n return `(${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())})`;\n}\nconst accountNameByCode = computed(() => {\n const map = new Map<string, string>();\n for (const account of props.accounts) map.set(account.code, account.name);\n return map;\n});\nfunction accountNameFor(code: string): string | null {\n return accountNameByCode.value.get(code) ?? null;\n}\n\n// Close button on the selected row's lines cell. Has to clear BOTH\n// expandedEntryId AND entryBeingEdited — if the user clicks Edit\n// (which sets entryBeingEdited) and then clicks Close, leaving\n// entryBeingEdited stale would block reopening: toggleExpanded's\n// edit-mode guard early-returns when entryBeingEdited.id matches the\n// clicked row, so the user could never reopen that entry from the\n// list. Issue surfaced by the CodeRabbit review on PR #1161.\nfunction onCloseDetail(): void {\n expandedEntryId.value = null;\n entryBeingEdited.value = null;\n}\n\nfunction toggleExpanded(entryId: string): void {\n // While the row is in edit mode for itself, ignore clicks on the\n // row chrome (date / kind / memo / lines cells) — the user is\n // actively typing into the form below and a stray cell click\n // shouldn't collapse the panel. Cancel / Submit on the form, or\n // clicking a different row, are the deliberate exits.\n if (entryBeingEdited.value?.id === entryId) return;\n expandedEntryId.value = expandedEntryId.value === entryId ? null : entryId;\n // Switching to a different row (or collapsing) drops any\n // in-progress edit on the prior row.\n entryBeingEdited.value = null;\n}\n\nfunction onKeyToggle(event: KeyboardEvent, entryId: string): void {\n if (event.repeat) return;\n toggleExpanded(entryId);\n}\n\nfunction entryHasTaxIds(entry: JournalEntry): boolean {\n return entry.lines.some((line) => Boolean(line.taxRegistrationId));\n}\n\nfunction sumLines(lines: JournalLine[], pick: (line: JournalLine) => number | undefined): number {\n return lines.reduce((acc, line) => acc + (pick(line) ?? 0), 0);\n}\n\nfunction entryDebitTotal(entry: JournalEntry): number {\n return sumLines(entry.lines, (line) => line.debit);\n}\n\nfunction entryCreditTotal(entry: JournalEntry): number {\n return sumLines(entry.lines, (line) => line.credit);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n const result = await getJournalEntries({\n bookId: props.bookId,\n from: range.value.from || undefined,\n to: range.value.to || undefined,\n accountCode: accountCode.value || undefined,\n });\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n entries.value = [];\n serverVoidedIds.value = [];\n return;\n }\n entries.value = result.data.entries;\n serverVoidedIds.value = result.data.voidedEntryIds;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nconst filteredEntries = computed(() => entries.value);\n\n// Visible-list view that pins the entry currently being edited at\n// the top when a filter change or pubsub-driven refetch would\n// otherwise drop it from `filteredEntries`. Without this, the\n// in-place edit form (which is nested under the row's v-if /\n// v-for) would unmount and silently discard the user's draft when:\n// • the user adjusts the date range or account filter,\n// • a sibling tab / LLM tool voids the entry out-of-band and the\n// SSE pubsub bumps `bookVersion`, refetching this list,\n// • or a sibling tab / LLM tool deletes the underlying book.\n// Pinning the editing entry from the local snapshot (entryBeingEdited)\n// keeps the form mounted across all three. The pinned row sits at\n// the top of the table while editing; on submit / cancel the\n// snapshot clears and the list reverts to filteredEntries.\nconst visibleEntries = computed<JournalEntry[]>(() => {\n const list = filteredEntries.value;\n const editing = entryBeingEdited.value;\n if (editing && !list.some((entry) => entry.id === editing.id)) {\n return [editing, ...list];\n }\n return list;\n});\n\n// Set of original entry ids that have been voided. The server\n// computes this from the *unfiltered* journal (so an account-filtered\n// query — which drops void-marker rows because they have no lines —\n// still strikes out the cancelled original). Source of truth on the\n// server is `voidedIdSet()` in journal.ts.\nconst voidedEntryIds = computed(() => new Set(serverVoidedIds.value));\n\nasync function onVoid(entry: JournalEntry): Promise<void> {\n // Single dialog: the prompt is the confirmation. Cancelling\n // (returning null) cancels the void; entering empty text or a\n // reason proceeds.\n const reason = window.prompt(t(\"pluginAccounting.journalList.voidReason\"));\n if (reason === null) return;\n try {\n const result = await voidEntry({ entryId: entry.id, reason: reason || undefined, bookId: props.bookId });\n if (!result.ok) error.value = result.error;\n } catch (err) {\n error.value = errorMessage(err);\n }\n}\n\n// Reset to current-year window whenever the active book or its\n// fiscal-year end changes. Keeps a custom range from leaking across\n// books and follows a settings-driven shift in fiscalYearEnd.\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\nwatch(() => [props.bookId, props.version, range.value.from, range.value.to, accountCode.value], refresh, { immediate: true });\n\n// Pending preselect: the parent hands us an id via `preselectEntryId`,\n// but the matching entry may not be in `entries` yet (the SSE-driven\n// refetch lands on its own clock). Stash it here, then the\n// [pendingPreselectId, entries] watcher below consumes it once the\n// row actually exists in the list — and clears it so subsequent\n// unrelated refetches (void events, sibling-tab edits) don't\n// re-expand a stale target.\nconst pendingPreselectId = ref<string | null>(null);\n\nwatch(\n () => props.preselectEntryId,\n (incoming) => {\n if (incoming) pendingPreselectId.value = incoming;\n },\n // immediate: true so a late JournalList mount (the View defers our\n // mount until refetchBooks resolves activeBookId) still captures\n // a preselect the parent had already set — without this, a normal\n // watcher misses the \"initial value is the target value\" case.\n { immediate: true },\n);\n\nwatch([pendingPreselectId, entries], async ([targetId, list]) => {\n if (!targetId) return;\n if (!list.some((entry) => entry.id === targetId)) return;\n // Always emit `preselectConsumed` (whether we expand or bail) so\n // the parent can drop its `journalPreselectEntryId` ref. Without\n // this one-shot signal, leaving and returning to the journal tab\n // (v-if remount) replays the immediate prop watcher against the\n // stale value, re-expanding an old row the user has already moved\n // past. Issue raised by the Codex automated review on PR #1158.\n if (entryBeingEdited.value) {\n // Don't overwrite an in-progress edit on another row — the\n // user's draft matters more than the highlight. Drop pending so\n // we don't keep retrying every refetch, and signal consumed so\n // the parent doesn't keep re-handing us the same id.\n pendingPreselectId.value = null;\n emit(\"preselectConsumed\");\n return;\n }\n expandedEntryId.value = targetId;\n await nextTick();\n const row =\n document.querySelector(`[data-testid=\"accounting-journal-row-${targetId}\"]`) ??\n document.querySelector(`[data-testid=\"accounting-journal-row-voided-${targetId}\"]`);\n row?.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n pendingPreselectId.value = null;\n emit(\"preselectConsumed\");\n});\n</script>\n\n<style scoped>\n/* Selection frame for the expanded entry. Borders go on the cells\n (not the <tr>) because border-collapse: collapse — Tailwind's\n default — eats <tr>-level borders/box-shadows. The entry row owns\n top/left/right; the detail-panel row directly below owns\n left/right/bottom, so together they read as one rectangle around\n the selection. Color matches the focus-ring blue used elsewhere\n in this list. */\n.row-selected > td {\n background-color: rgb(239 246 255); /* tailwind blue-50 */\n border-top: 2px solid rgb(59 130 246); /* tailwind blue-500 */\n}\n.row-selected > td:first-child {\n border-left: 2px solid rgb(59 130 246);\n}\n.row-selected > td:last-child {\n border-right: 2px solid rgb(59 130 246);\n}\n.detail-selected > td {\n background-color: rgb(239 246 255);\n border-left: 2px solid rgb(59 130 246);\n border-right: 2px solid rgb(59 130 246);\n border-bottom: 2px solid rgb(59 130 246);\n}\n</style>\n","<template>\n <form class=\"flex flex-col gap-3\" data-testid=\"accounting-opening-form\" @submit.prevent=\"onSubmit\">\n <div class=\"flex items-center justify-between gap-2\">\n <h3 class=\"text-base font-semibold\">{{ t(\"pluginAccounting.openingForm.title\") }}</h3>\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-opening-manage-accounts\"\n @click=\"showAccountsModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.openingForm.explainer\") }}</p>\n <p class=\"text-xs text-blue-600\" data-testid=\"accounting-opening-empty-hint\">{{ t(\"pluginAccounting.openingForm.emptyHint\") }}</p>\n <div v-if=\"existing\" class=\"text-xs text-gray-500\" data-testid=\"accounting-opening-existing\">\n {{ t(\"pluginAccounting.openingForm.setBy\", { date: existing.date }) }}\n <span v-if=\"existing\" class=\"text-amber-600 ml-2\">{{ t(\"pluginAccounting.openingForm.replaceWarning\") }}</span>\n </div>\n <p v-else class=\"text-xs text-gray-400\" data-testid=\"accounting-opening-none\">{{ t(\"pluginAccounting.openingForm.none\") }}</p>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-fit\">\n {{ t(\"pluginAccounting.openingForm.asOfLabel\") }}\n <input v-model=\"asOfDate\" type=\"date\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-opening-asof\" />\n </label>\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"account in bsAccounts\" :key=\"account.code\" class=\"border-b border-gray-100\">\n <td class=\"py-1 px-2\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ account.code }}</span>\n <span>{{ account.name }}</span>\n <span class=\"ml-2 text-xs text-gray-400\">{{ account.type }}</span>\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"rows[account.code].debit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right\"\n :data-testid=\"`accounting-opening-debit-${account.code}`\"\n @input=\"onDebitInput(account.code)\"\n />\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"rows[account.code].credit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right\"\n :data-testid=\"`accounting-opening-credit-${account.code}`\"\n @input=\"onCreditInput(account.code)\"\n />\n </td>\n </tr>\n </tbody>\n </table>\n <div class=\"flex items-center justify-between\">\n <span class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.openingForm.explainer2\") }}</span>\n <span :class=\"balanced ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-opening-balance\">\n {{ balanced ? t(\"pluginAccounting.entryForm.balanced\") : t(\"pluginAccounting.entryForm.imbalance\", { amount: imbalanceText }) }}\n </span>\n </div>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-opening-error\">{{ error }}</p>\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-opening-success\">{{ successMessage }}</p>\n <div class=\"flex justify-end\">\n <button\n type=\"submit\"\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"!balanced || submitting\"\n data-testid=\"accounting-opening-submit\"\n >\n {{ submitting ? t(\"pluginAccounting.entryForm.submitting\") : t(\"pluginAccounting.openingForm.submit\") }}\n </button>\n </div>\n </form>\n <!-- Sibling of the parent <form> on purpose: the modal renders\n its own <form @submit.prevent> for the inline editor, and\n nesting <form>s is invalid HTML that breaks Enter-key submit\n routing in some browsers. Vue 3 multi-root templates let us\n keep the markup flat with no wrapper div. -->\n <AccountsModal v-if=\"showAccountsModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showAccountsModal = false\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { getOpeningBalances, setOpeningBalances, type Account, type JournalEntry, type JournalLine } from \"../api\";\nimport { formatAmount, inputStepFor, localDateString } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport AccountsModal from \"./AccountsModal.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[]; currency: string; version: number }>();\nconst emit = defineEmits<{ submitted: [] }>();\n\nconst showAccountsModal = ref(false);\n\ninterface OpeningRow {\n debit: number | null;\n credit: number | null;\n}\n\nconst asOfDate = ref(localDateString());\nconst rows = ref<Record<string, OpeningRow>>({});\nconst existing = ref<JournalEntry | null>(null);\nconst submitting = ref(false);\nconst error = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\nconst { begin: beginLoad, isCurrent: isCurrentLoad } = useLatestRequest();\n\nconst bsAccounts = computed(() =>\n props.accounts.filter((account) => (account.type === \"asset\" || account.type === \"liability\" || account.type === \"equity\") && account.active !== false),\n);\n\nfunction ensureRows(): void {\n for (const account of bsAccounts.value) {\n if (!rows.value[account.code]) rows.value[account.code] = { debit: null, credit: null };\n }\n}\n\nfunction onDebitInput(code: string): void {\n const row = rows.value[code];\n if (row.debit !== null && row.debit !== 0) row.credit = null;\n}\nfunction onCreditInput(code: string): void {\n const row = rows.value[code];\n if (row.credit !== null && row.credit !== 0) row.debit = null;\n}\n\nconst imbalance = computed<number>(() => {\n // Iterate the live bsAccounts (already active-filtered) rather\n // than rows.value keys so a row for a now-inactive account\n // doesn't tilt `balanced` against what `toApiLines` will\n // actually post.\n let sum = 0;\n for (const account of bsAccounts.value) {\n const row = rows.value[account.code];\n if (!row) continue;\n if (typeof row.debit === \"number\") sum += row.debit;\n if (typeof row.credit === \"number\") sum -= row.credit;\n }\n return sum;\n});\n// An all-empty form is valid: it submits as a zero-line opening\n// marker so the user can unlock the rest of the UI without\n// committing to specific balances on day one.\nconst balanced = computed(() => Math.abs(imbalance.value) <= 0.005);\nconst imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));\nconst step = computed(() => inputStepFor(props.currency));\n\nfunction isPositiveAmount(value: unknown): value is number {\n // Robust against the empty string `v-model.number` produces when\n // the user clears a previously-typed field — without this, the\n // skip condition `value === 0` was false for `\"\"` and the form\n // emitted ghost lines like `{accountCode: \"3000\"}` with no\n // amount on either side.\n return typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\nfunction toApiLines(): JournalLine[] {\n const out: JournalLine[] = [];\n // Iterate the visible bsAccounts list (which already filters\n // out inactive accounts) rather than `rows.value` keys. A row\n // for an account that was active when the user typed amounts\n // and then got deactivated mid-edit would otherwise still post —\n // the row stays in the map even after the v-for stops rendering\n // it, so iterating keys would silently land entries on a\n // soft-deleted account.\n for (const account of bsAccounts.value) {\n const row = rows.value[account.code];\n if (!row) continue;\n const debitOk = isPositiveAmount(row.debit);\n const creditOk = isPositiveAmount(row.credit);\n if (!debitOk && !creditOk) continue;\n const line: JournalLine = { accountCode: account.code };\n if (debitOk) line.debit = row.debit as number;\n if (creditOk) line.credit = row.credit as number;\n out.push(line);\n }\n return out;\n}\n\nfunction freshRows(): Record<string, OpeningRow> {\n const out: Record<string, OpeningRow> = {};\n for (const account of bsAccounts.value) out[account.code] = { debit: null, credit: null };\n return out;\n}\n\nasync function loadExisting(): Promise<void> {\n // Always start from a fresh row map so a book without an\n // opening doesn't inherit the previous book's draft values.\n const token = beginLoad();\n const next = freshRows();\n const result = await getOpeningBalances(props.bookId);\n // Drop the result if the user has switched books since this\n // call started — otherwise stale rows would land on the new\n // book's form.\n if (!isCurrentLoad(token)) return;\n if (!result.ok) {\n existing.value = null;\n rows.value = next;\n return;\n }\n existing.value = result.data.opening;\n if (result.data.opening) {\n asOfDate.value = result.data.opening.date;\n for (const line of result.data.opening.lines) {\n next[line.accountCode] = { debit: line.debit ?? null, credit: line.credit ?? null };\n }\n } else {\n asOfDate.value = localDateString();\n }\n rows.value = next;\n}\n\nasync function onSubmit(): Promise<void> {\n if (submitting.value || !balanced.value) return;\n submitting.value = true;\n error.value = null;\n successMessage.value = null;\n try {\n const result = await setOpeningBalances({ bookId: props.bookId, asOfDate: asOfDate.value, lines: toApiLines() });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n successMessage.value = t(\"pluginAccounting.openingForm.success\");\n emit(\"submitted\");\n } catch (err) {\n error.value = errorMessage(err);\n } finally {\n submitting.value = false;\n }\n}\n\nwatch(\n () => [props.bookId, props.version, props.accounts.length],\n () => {\n ensureRows();\n void loadExisting();\n },\n { immediate: true },\n);\n</script>\n\n<style scoped>\n/* Hide the WebKit / Firefox spin buttons on amount inputs. The\n step attribute still controls validation; this is purely UI.\n Accounting amount fields don't benefit from a spinner — users\n type the number and the up/down arrows just clutter the row. */\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n appearance: textfield;\n}\n</style>\n","<template>\n <form class=\"flex flex-col gap-3\" data-testid=\"accounting-opening-form\" @submit.prevent=\"onSubmit\">\n <div class=\"flex items-center justify-between gap-2\">\n <h3 class=\"text-base font-semibold\">{{ t(\"pluginAccounting.openingForm.title\") }}</h3>\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-opening-manage-accounts\"\n @click=\"showAccountsModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.openingForm.explainer\") }}</p>\n <p class=\"text-xs text-blue-600\" data-testid=\"accounting-opening-empty-hint\">{{ t(\"pluginAccounting.openingForm.emptyHint\") }}</p>\n <div v-if=\"existing\" class=\"text-xs text-gray-500\" data-testid=\"accounting-opening-existing\">\n {{ t(\"pluginAccounting.openingForm.setBy\", { date: existing.date }) }}\n <span v-if=\"existing\" class=\"text-amber-600 ml-2\">{{ t(\"pluginAccounting.openingForm.replaceWarning\") }}</span>\n </div>\n <p v-else class=\"text-xs text-gray-400\" data-testid=\"accounting-opening-none\">{{ t(\"pluginAccounting.openingForm.none\") }}</p>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-fit\">\n {{ t(\"pluginAccounting.openingForm.asOfLabel\") }}\n <input v-model=\"asOfDate\" type=\"date\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-opening-asof\" />\n </label>\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"account in bsAccounts\" :key=\"account.code\" class=\"border-b border-gray-100\">\n <td class=\"py-1 px-2\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ account.code }}</span>\n <span>{{ account.name }}</span>\n <span class=\"ml-2 text-xs text-gray-400\">{{ account.type }}</span>\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"rows[account.code].debit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right\"\n :data-testid=\"`accounting-opening-debit-${account.code}`\"\n @input=\"onDebitInput(account.code)\"\n />\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"rows[account.code].credit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right\"\n :data-testid=\"`accounting-opening-credit-${account.code}`\"\n @input=\"onCreditInput(account.code)\"\n />\n </td>\n </tr>\n </tbody>\n </table>\n <div class=\"flex items-center justify-between\">\n <span class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.openingForm.explainer2\") }}</span>\n <span :class=\"balanced ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-opening-balance\">\n {{ balanced ? t(\"pluginAccounting.entryForm.balanced\") : t(\"pluginAccounting.entryForm.imbalance\", { amount: imbalanceText }) }}\n </span>\n </div>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-opening-error\">{{ error }}</p>\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-opening-success\">{{ successMessage }}</p>\n <div class=\"flex justify-end\">\n <button\n type=\"submit\"\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"!balanced || submitting\"\n data-testid=\"accounting-opening-submit\"\n >\n {{ submitting ? t(\"pluginAccounting.entryForm.submitting\") : t(\"pluginAccounting.openingForm.submit\") }}\n </button>\n </div>\n </form>\n <!-- Sibling of the parent <form> on purpose: the modal renders\n its own <form @submit.prevent> for the inline editor, and\n nesting <form>s is invalid HTML that breaks Enter-key submit\n routing in some browsers. Vue 3 multi-root templates let us\n keep the markup flat with no wrapper div. -->\n <AccountsModal v-if=\"showAccountsModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showAccountsModal = false\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { getOpeningBalances, setOpeningBalances, type Account, type JournalEntry, type JournalLine } from \"../api\";\nimport { formatAmount, inputStepFor, localDateString } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport AccountsModal from \"./AccountsModal.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[]; currency: string; version: number }>();\nconst emit = defineEmits<{ submitted: [] }>();\n\nconst showAccountsModal = ref(false);\n\ninterface OpeningRow {\n debit: number | null;\n credit: number | null;\n}\n\nconst asOfDate = ref(localDateString());\nconst rows = ref<Record<string, OpeningRow>>({});\nconst existing = ref<JournalEntry | null>(null);\nconst submitting = ref(false);\nconst error = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\nconst { begin: beginLoad, isCurrent: isCurrentLoad } = useLatestRequest();\n\nconst bsAccounts = computed(() =>\n props.accounts.filter((account) => (account.type === \"asset\" || account.type === \"liability\" || account.type === \"equity\") && account.active !== false),\n);\n\nfunction ensureRows(): void {\n for (const account of bsAccounts.value) {\n if (!rows.value[account.code]) rows.value[account.code] = { debit: null, credit: null };\n }\n}\n\nfunction onDebitInput(code: string): void {\n const row = rows.value[code];\n if (row.debit !== null && row.debit !== 0) row.credit = null;\n}\nfunction onCreditInput(code: string): void {\n const row = rows.value[code];\n if (row.credit !== null && row.credit !== 0) row.debit = null;\n}\n\nconst imbalance = computed<number>(() => {\n // Iterate the live bsAccounts (already active-filtered) rather\n // than rows.value keys so a row for a now-inactive account\n // doesn't tilt `balanced` against what `toApiLines` will\n // actually post.\n let sum = 0;\n for (const account of bsAccounts.value) {\n const row = rows.value[account.code];\n if (!row) continue;\n if (typeof row.debit === \"number\") sum += row.debit;\n if (typeof row.credit === \"number\") sum -= row.credit;\n }\n return sum;\n});\n// An all-empty form is valid: it submits as a zero-line opening\n// marker so the user can unlock the rest of the UI without\n// committing to specific balances on day one.\nconst balanced = computed(() => Math.abs(imbalance.value) <= 0.005);\nconst imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));\nconst step = computed(() => inputStepFor(props.currency));\n\nfunction isPositiveAmount(value: unknown): value is number {\n // Robust against the empty string `v-model.number` produces when\n // the user clears a previously-typed field — without this, the\n // skip condition `value === 0` was false for `\"\"` and the form\n // emitted ghost lines like `{accountCode: \"3000\"}` with no\n // amount on either side.\n return typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\nfunction toApiLines(): JournalLine[] {\n const out: JournalLine[] = [];\n // Iterate the visible bsAccounts list (which already filters\n // out inactive accounts) rather than `rows.value` keys. A row\n // for an account that was active when the user typed amounts\n // and then got deactivated mid-edit would otherwise still post —\n // the row stays in the map even after the v-for stops rendering\n // it, so iterating keys would silently land entries on a\n // soft-deleted account.\n for (const account of bsAccounts.value) {\n const row = rows.value[account.code];\n if (!row) continue;\n const debitOk = isPositiveAmount(row.debit);\n const creditOk = isPositiveAmount(row.credit);\n if (!debitOk && !creditOk) continue;\n const line: JournalLine = { accountCode: account.code };\n if (debitOk) line.debit = row.debit as number;\n if (creditOk) line.credit = row.credit as number;\n out.push(line);\n }\n return out;\n}\n\nfunction freshRows(): Record<string, OpeningRow> {\n const out: Record<string, OpeningRow> = {};\n for (const account of bsAccounts.value) out[account.code] = { debit: null, credit: null };\n return out;\n}\n\nasync function loadExisting(): Promise<void> {\n // Always start from a fresh row map so a book without an\n // opening doesn't inherit the previous book's draft values.\n const token = beginLoad();\n const next = freshRows();\n const result = await getOpeningBalances(props.bookId);\n // Drop the result if the user has switched books since this\n // call started — otherwise stale rows would land on the new\n // book's form.\n if (!isCurrentLoad(token)) return;\n if (!result.ok) {\n existing.value = null;\n rows.value = next;\n return;\n }\n existing.value = result.data.opening;\n if (result.data.opening) {\n asOfDate.value = result.data.opening.date;\n for (const line of result.data.opening.lines) {\n next[line.accountCode] = { debit: line.debit ?? null, credit: line.credit ?? null };\n }\n } else {\n asOfDate.value = localDateString();\n }\n rows.value = next;\n}\n\nasync function onSubmit(): Promise<void> {\n if (submitting.value || !balanced.value) return;\n submitting.value = true;\n error.value = null;\n successMessage.value = null;\n try {\n const result = await setOpeningBalances({ bookId: props.bookId, asOfDate: asOfDate.value, lines: toApiLines() });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n successMessage.value = t(\"pluginAccounting.openingForm.success\");\n emit(\"submitted\");\n } catch (err) {\n error.value = errorMessage(err);\n } finally {\n submitting.value = false;\n }\n}\n\nwatch(\n () => [props.bookId, props.version, props.accounts.length],\n () => {\n ensureRows();\n void loadExisting();\n },\n { immediate: true },\n);\n</script>\n\n<style scoped>\n/* Hide the WebKit / Firefox spin buttons on amount inputs. The\n step attribute still controls validation; this is purely UI.\n Accounting amount fields don't benefit from a spinner — users\n type the number and the up/down arrows just clutter the row. */\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n appearance: textfield;\n}\n</style>\n","<template>\n <!-- Full-tab chart-of-accounts list. Distinct from AccountsModal\n (called from the entry / opening forms): this one fills the\n canvas, surfaces a single \"Manage accounts\" button at the top,\n and emits `selectAccount` so the parent View can route the\n click into the Ledger tab pre-filtered to that account. -->\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-accounts-list\">\n <div class=\"flex flex-wrap items-center justify-end gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-accounts-manage\"\n @click=\"showManageModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <section v-for=\"group in groups\" :key=\"group.type\" class=\"flex flex-col gap-1\">\n <h4 class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ t(`pluginAccounting.accounts.sectionTitle.${group.type}`) }}</h4>\n <p v-if=\"group.accounts.length === 0\" class=\"text-xs text-gray-400 italic px-1\">{{ t(\"pluginAccounting.accounts.listEmpty\") }}</p>\n <ul v-else class=\"flex flex-col\">\n <li\n v-for=\"account in group.accounts\"\n :key=\"account.code\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: account.code, name: account.name })\"\n class=\"flex items-center gap-3 px-2 py-1.5 border-b border-gray-100 hover:bg-blue-50 cursor-pointer text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 rounded\"\n :data-testid=\"`accounting-account-row-${account.code}`\"\n @click=\"onSelect(account)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, account)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, account)\"\n >\n <span class=\"font-mono text-xs w-16 shrink-0\">{{ account.code }}</span>\n <span class=\"text-sm flex-1 min-w-0 truncate\">{{ account.name }}</span>\n </li>\n </ul>\n </section>\n <AccountsModal v-if=\"showManageModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showManageModal = false\" @changed=\"onAccountsChanged\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type { Account, AccountType } from \"../api\";\nimport AccountsModal from \"./AccountsModal.vue\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[] }>();\nconst emit = defineEmits<{ selectAccount: [code: string]; changed: [] }>();\n\nconst ACCOUNT_TYPES: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\n\nconst showManageModal = ref(false);\n\ninterface AccountGroup {\n type: AccountType;\n accounts: Account[];\n}\n\nfunction byCode(left: Account, right: Account): number {\n return left.code.localeCompare(right.code);\n}\n\n// Soft-deleted accounts (active === false) are hidden — managing\n// them lives in the Manage Accounts modal, where Reactivate is one\n// click away.\nconst groups = computed<AccountGroup[]>(() =>\n ACCOUNT_TYPES.map((type) => ({\n type,\n accounts: props.accounts\n .filter((account) => account.type === type && account.active !== false)\n .slice()\n .sort(byCode),\n })),\n);\n\nfunction onSelect(account: Account): void {\n emit(\"selectAccount\", account.code);\n}\n\n// Keyboard activation: Enter / Space on a focused row. The\n// `.prevent.self` modifiers in the template stop the default scroll\n// (Space) and ensure we don't fire when the event bubbles up from\n// a focused descendant (currently none, but defensive for future\n// row content).\nfunction onKeyActivate(event: KeyboardEvent, account: Account): void {\n if (event.repeat) return;\n emit(\"selectAccount\", account.code);\n}\n\nfunction onAccountsChanged(): void {\n // Forward to the parent — `bookVersion` already drives the\n // accounts refetch in View.vue, so the list updates without us\n // doing anything extra. Bubble the event in case a future\n // consumer needs it.\n emit(\"changed\");\n}\n</script>\n","<template>\n <!-- Full-tab chart-of-accounts list. Distinct from AccountsModal\n (called from the entry / opening forms): this one fills the\n canvas, surfaces a single \"Manage accounts\" button at the top,\n and emits `selectAccount` so the parent View can route the\n click into the Ledger tab pre-filtered to that account. -->\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-accounts-list\">\n <div class=\"flex flex-wrap items-center justify-end gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-accounts-manage\"\n @click=\"showManageModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <section v-for=\"group in groups\" :key=\"group.type\" class=\"flex flex-col gap-1\">\n <h4 class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ t(`pluginAccounting.accounts.sectionTitle.${group.type}`) }}</h4>\n <p v-if=\"group.accounts.length === 0\" class=\"text-xs text-gray-400 italic px-1\">{{ t(\"pluginAccounting.accounts.listEmpty\") }}</p>\n <ul v-else class=\"flex flex-col\">\n <li\n v-for=\"account in group.accounts\"\n :key=\"account.code\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: account.code, name: account.name })\"\n class=\"flex items-center gap-3 px-2 py-1.5 border-b border-gray-100 hover:bg-blue-50 cursor-pointer text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 rounded\"\n :data-testid=\"`accounting-account-row-${account.code}`\"\n @click=\"onSelect(account)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, account)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, account)\"\n >\n <span class=\"font-mono text-xs w-16 shrink-0\">{{ account.code }}</span>\n <span class=\"text-sm flex-1 min-w-0 truncate\">{{ account.name }}</span>\n </li>\n </ul>\n </section>\n <AccountsModal v-if=\"showManageModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showManageModal = false\" @changed=\"onAccountsChanged\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type { Account, AccountType } from \"../api\";\nimport AccountsModal from \"./AccountsModal.vue\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[] }>();\nconst emit = defineEmits<{ selectAccount: [code: string]; changed: [] }>();\n\nconst ACCOUNT_TYPES: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\n\nconst showManageModal = ref(false);\n\ninterface AccountGroup {\n type: AccountType;\n accounts: Account[];\n}\n\nfunction byCode(left: Account, right: Account): number {\n return left.code.localeCompare(right.code);\n}\n\n// Soft-deleted accounts (active === false) are hidden — managing\n// them lives in the Manage Accounts modal, where Reactivate is one\n// click away.\nconst groups = computed<AccountGroup[]>(() =>\n ACCOUNT_TYPES.map((type) => ({\n type,\n accounts: props.accounts\n .filter((account) => account.type === type && account.active !== false)\n .slice()\n .sort(byCode),\n })),\n);\n\nfunction onSelect(account: Account): void {\n emit(\"selectAccount\", account.code);\n}\n\n// Keyboard activation: Enter / Space on a focused row. The\n// `.prevent.self` modifiers in the template stop the default scroll\n// (Space) and ensure we don't fire when the event bubbles up from\n// a focused descendant (currently none, but defensive for future\n// row content).\nfunction onKeyActivate(event: KeyboardEvent, account: Account): void {\n if (event.repeat) return;\n emit(\"selectAccount\", account.code);\n}\n\nfunction onAccountsChanged(): void {\n // Forward to the parent — `bookVersion` already drives the\n // accounts refetch in View.vue, so the list updates without us\n // doing anything extra. Bubble the event in case a future\n // consumer needs it.\n emit(\"changed\");\n}\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-ledger\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.ledger.selectAccount\") }}\n <select v-model=\"accountCode\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-ledger-account\">\n <option value=\"\">{{ DASH }}</option>\n <option v-for=\"account in selectableAccounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </label>\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"ledger\">\n <table class=\"w-full text-sm\" :data-testid=\"showTaxRegistrationColumn ? 'accounting-ledger-table-with-tax-id' : 'accounting-ledger-table'\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.ledger.columns.date\") }}</th>\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.ledger.columns.memo\") }}</th>\n <th v-if=\"showTaxRegistrationColumn\" class=\"text-left py-1 px-2 w-40\">\n {{ t(\"pluginAccounting.ledger.columns.taxRegistrationId\") }}\n </th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.debit\") }}</th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.credit\") }}</th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.balance\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr\n v-for=\"row in ledger.rows\"\n :key=\"`${row.entryId}-${row.date}`\"\n :class=\"row.kind === 'void' || row.kind === 'void-marker' ? 'text-gray-400 line-through' : ''\"\n class=\"border-b border-gray-100\"\n >\n <td class=\"py-1 px-2 whitespace-nowrap\">{{ row.date }}</td>\n <td class=\"py-1 px-2\">\n <span v-if=\"row.memo\">{{ row.memo }}</span>\n </td>\n <td v-if=\"showTaxRegistrationColumn\" class=\"py-1 px-2 font-mono text-xs\">\n <span v-if=\"row.taxRegistrationId\">{{ row.taxRegistrationId }}</span>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <span v-if=\"row.debit\">{{ formatAmount(row.debit) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <span v-if=\"row.credit\">{{ formatAmount(row.credit) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(row.runningBalance) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td :colspan=\"showTaxRegistrationColumn ? 5 : 4\" class=\"py-1 px-2 text-right\">\n {{ t(\"pluginAccounting.ledger.closingBalance\") }}\n </td>\n <td class=\"py-1 px-2 text-right\">{{ formatAmount(ledger.closingBalance) }}</td>\n </tr>\n </tfoot>\n </table>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { getLedger, type Account, type Ledger, type ReportPeriod } from \"../api\";\nimport { formatAmount as formatAmountWithCurrency, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd } from \"../../shared\";\nimport { isTaxAccountCode } from \"./accountNumbering\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n bookId: string;\n accounts: Account[];\n currency: string;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n /** Optional account to preselect (Accounts tab → click). Updates\n * via the watcher below — assigning to the local `accountCode`\n * ref keeps the dropdown's v-model authoritative for user edits. */\n preselectAccountCode?: string;\n}>();\n\nconst DASH = \"—\";\nconst accountCode = ref(\"\");\nconst ledger = ref<Ledger | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\n// Default range = current fiscal year. Re-evaluated when bookId or\n// fiscalYearEnd changes (see watcher) so switching books resets to a\n// sensible window rather than carrying the prior book's custom edits.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n return `${account.name} (${account.code})`;\n}\n\n// Hide deactivated accounts from the ledger picker; historical\n// entries on a soft-deleted account are still inspectable via\n// the journal-list filter (which intentionally shows every code\n// so the past stays queryable).\nconst selectableAccounts = computed<Account[]>(() => props.accounts.filter((account) => account.active !== false));\n\n// Surface the T-number column when the active account is in the\n// input-tax band (14xx — e.g. 1400 Input Tax Receivable).\n// Convention-driven so any custom account a user adds in the band\n// participates without an opt-in flag. 24xx (Sales Tax Payable\n// and friends) intentionally doesn't get the column — the\n// counterparty registration ID matters for input-tax-credit\n// eligibility on purchases, not for the seller-side liability.\nconst showTaxRegistrationColumn = computed<boolean>(() => {\n if (!ledger.value) return false;\n return isTaxAccountCode(ledger.value.accountCode);\n});\n\n// Build a ReportPeriod from the current range. Both ends empty = no\n// filter (full history); either end alone gets a sentinel on the\n// other side so the server-side range filter still applies.\nfunction periodFromRange(value: DateRange): ReportPeriod | undefined {\n if (value.from === \"\" && value.to === \"\") return undefined;\n return { kind: \"range\", from: value.from || \"0000-01-01\", to: value.to || \"9999-12-31\" };\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n if (!accountCode.value) {\n ledger.value = null;\n error.value = null;\n loading.value = false;\n return;\n }\n loading.value = true;\n error.value = null;\n try {\n const result = await getLedger(accountCode.value, periodFromRange(range.value), props.bookId);\n // Drop the result if a newer refresh started (bookId or\n // accountCode changed under us) — otherwise a slower earlier\n // request could overwrite the fresh ledger.\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n ledger.value = null;\n return;\n }\n ledger.value = result.data.ledger;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\n// Reset to current-year window AND drop the selected account\n// whenever the active book or its fiscal-year end changes. Without\n// the accountCode reset, switching from book A (cash=1000) to book\n// B (which may not even define 1000) fires a getLedger for a\n// missing code and surfaces an avoidable 404. The range reset\n// follows the same logic — a custom window from book A is\n// meaningless against book B's entries.\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n accountCode.value = \"\";\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\n// Apply parent-supplied preselection (Accounts tab → click). The\n// watcher fires on both initial mount (with `immediate`) and on\n// every prop change so re-clicking the same account from the\n// Accounts tab while already on the Ledger still routes through.\n// Resets the range to the current fiscal year on each preselect so\n// a stale custom window the user left behind on the Ledger doesn't\n// hide the entries the Accounts tab handed off.\nwatch(\n () => props.preselectAccountCode,\n (next) => {\n if (!next) return;\n accountCode.value = next;\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n { immediate: true },\n);\n\nwatch(() => [props.bookId, props.version, accountCode.value, range.value.from, range.value.to], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-ledger\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.ledger.selectAccount\") }}\n <select v-model=\"accountCode\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-ledger-account\">\n <option value=\"\">{{ DASH }}</option>\n <option v-for=\"account in selectableAccounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </label>\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"ledger\">\n <table class=\"w-full text-sm\" :data-testid=\"showTaxRegistrationColumn ? 'accounting-ledger-table-with-tax-id' : 'accounting-ledger-table'\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.ledger.columns.date\") }}</th>\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.ledger.columns.memo\") }}</th>\n <th v-if=\"showTaxRegistrationColumn\" class=\"text-left py-1 px-2 w-40\">\n {{ t(\"pluginAccounting.ledger.columns.taxRegistrationId\") }}\n </th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.debit\") }}</th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.credit\") }}</th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.balance\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr\n v-for=\"row in ledger.rows\"\n :key=\"`${row.entryId}-${row.date}`\"\n :class=\"row.kind === 'void' || row.kind === 'void-marker' ? 'text-gray-400 line-through' : ''\"\n class=\"border-b border-gray-100\"\n >\n <td class=\"py-1 px-2 whitespace-nowrap\">{{ row.date }}</td>\n <td class=\"py-1 px-2\">\n <span v-if=\"row.memo\">{{ row.memo }}</span>\n </td>\n <td v-if=\"showTaxRegistrationColumn\" class=\"py-1 px-2 font-mono text-xs\">\n <span v-if=\"row.taxRegistrationId\">{{ row.taxRegistrationId }}</span>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <span v-if=\"row.debit\">{{ formatAmount(row.debit) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <span v-if=\"row.credit\">{{ formatAmount(row.credit) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(row.runningBalance) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td :colspan=\"showTaxRegistrationColumn ? 5 : 4\" class=\"py-1 px-2 text-right\">\n {{ t(\"pluginAccounting.ledger.closingBalance\") }}\n </td>\n <td class=\"py-1 px-2 text-right\">{{ formatAmount(ledger.closingBalance) }}</td>\n </tr>\n </tfoot>\n </table>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { getLedger, type Account, type Ledger, type ReportPeriod } from \"../api\";\nimport { formatAmount as formatAmountWithCurrency, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd } from \"../../shared\";\nimport { isTaxAccountCode } from \"./accountNumbering\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n bookId: string;\n accounts: Account[];\n currency: string;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n /** Optional account to preselect (Accounts tab → click). Updates\n * via the watcher below — assigning to the local `accountCode`\n * ref keeps the dropdown's v-model authoritative for user edits. */\n preselectAccountCode?: string;\n}>();\n\nconst DASH = \"—\";\nconst accountCode = ref(\"\");\nconst ledger = ref<Ledger | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\n// Default range = current fiscal year. Re-evaluated when bookId or\n// fiscalYearEnd changes (see watcher) so switching books resets to a\n// sensible window rather than carrying the prior book's custom edits.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n return `${account.name} (${account.code})`;\n}\n\n// Hide deactivated accounts from the ledger picker; historical\n// entries on a soft-deleted account are still inspectable via\n// the journal-list filter (which intentionally shows every code\n// so the past stays queryable).\nconst selectableAccounts = computed<Account[]>(() => props.accounts.filter((account) => account.active !== false));\n\n// Surface the T-number column when the active account is in the\n// input-tax band (14xx — e.g. 1400 Input Tax Receivable).\n// Convention-driven so any custom account a user adds in the band\n// participates without an opt-in flag. 24xx (Sales Tax Payable\n// and friends) intentionally doesn't get the column — the\n// counterparty registration ID matters for input-tax-credit\n// eligibility on purchases, not for the seller-side liability.\nconst showTaxRegistrationColumn = computed<boolean>(() => {\n if (!ledger.value) return false;\n return isTaxAccountCode(ledger.value.accountCode);\n});\n\n// Build a ReportPeriod from the current range. Both ends empty = no\n// filter (full history); either end alone gets a sentinel on the\n// other side so the server-side range filter still applies.\nfunction periodFromRange(value: DateRange): ReportPeriod | undefined {\n if (value.from === \"\" && value.to === \"\") return undefined;\n return { kind: \"range\", from: value.from || \"0000-01-01\", to: value.to || \"9999-12-31\" };\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n if (!accountCode.value) {\n ledger.value = null;\n error.value = null;\n loading.value = false;\n return;\n }\n loading.value = true;\n error.value = null;\n try {\n const result = await getLedger(accountCode.value, periodFromRange(range.value), props.bookId);\n // Drop the result if a newer refresh started (bookId or\n // accountCode changed under us) — otherwise a slower earlier\n // request could overwrite the fresh ledger.\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n ledger.value = null;\n return;\n }\n ledger.value = result.data.ledger;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\n// Reset to current-year window AND drop the selected account\n// whenever the active book or its fiscal-year end changes. Without\n// the accountCode reset, switching from book A (cash=1000) to book\n// B (which may not even define 1000) fires a getLedger for a\n// missing code and surfaces an avoidable 404. The range reset\n// follows the same logic — a custom window from book A is\n// meaningless against book B's entries.\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n accountCode.value = \"\";\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\n// Apply parent-supplied preselection (Accounts tab → click). The\n// watcher fires on both initial mount (with `immediate`) and on\n// every prop change so re-clicking the same account from the\n// Accounts tab while already on the Ledger still routes through.\n// Resets the range to the current fiscal year on each preselect so\n// a stale custom window the user left behind on the Ledger doesn't\n// hide the entries the Accounts tab handed off.\nwatch(\n () => props.preselectAccountCode,\n (next) => {\n if (!next) return;\n accountCode.value = next;\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n { immediate: true },\n);\n\nwatch(() => [props.bookId, props.version, accountCode.value, range.value.from, range.value.to], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-balance-sheet\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.balanceSheet.shortcutLabel\") }}\n <select\n :value=\"selectedShortcut\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-bs-shortcut\"\n @change=\"onShortcutChange(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"\" hidden></option>\n <option value=\"thisMonth\">{{ t(\"pluginAccounting.balanceSheet.thisMonth\") }}</option>\n <option value=\"lastMonth\">{{ t(\"pluginAccounting.balanceSheet.lastMonth\") }}</option>\n <option value=\"lastQuarter\">{{ t(\"pluginAccounting.balanceSheet.lastQuarter\") }}</option>\n <option value=\"lastYear\">{{ t(\"pluginAccounting.balanceSheet.lastYear\") }}</option>\n </select>\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.balanceSheet.asOfLabel\") }}\n <input v-model=\"period\" type=\"month\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-bs-period\" />\n </label>\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"balanceSheet\">\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <section v-for=\"section in balanceSheet.sections\" :key=\"section.type\" class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ sectionLabel(section.type) }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in section.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100\"\n :class=\"\n isEarningsRow(row)\n ? 'italic text-gray-600'\n : 'hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400'\n \"\n :tabindex=\"isEarningsRow(row) ? -1 : 0\"\n :role=\"isEarningsRow(row) ? undefined : 'button'\"\n :aria-label=\"isEarningsRow(row) ? undefined : t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"isEarningsRow(row) ? undefined : `accounting-bs-row-${row.accountCode}`\"\n @click=\"onRowClick(row)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row)\"\n >\n <td class=\"py-1 px-1\">\n <span v-if=\"!isEarningsRow(row)\" class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ rowName(row) }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.balance) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(section.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n </div>\n <p :class=\"Math.abs(balanceSheet.imbalance) <= 0.01 ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-bs-imbalance\">\n {{ t(\"pluginAccounting.balanceSheet.imbalance\", { amount: formatAmount(balanceSheet.imbalance) }) }}\n </p>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { getBalanceSheet, type BalanceSheet } from \"../api\";\nimport {\n formatAmount as formatAmountWithCurrency,\n decemberOfPreviousYearString,\n lastMonthOfPreviousQuarterString,\n localMonthString,\n previousMonthString,\n} from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ bookId: string; currency: string; version: number }>();\nconst emit = defineEmits<{ selectAccount: [code: string] }>();\n\nconst period = ref(localMonthString());\nconst balanceSheet = ref<BalanceSheet | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nfunction sectionLabel(type: string): string {\n if (type === \"asset\") return t(\"pluginAccounting.balanceSheet.sections.asset\");\n if (type === \"liability\") return t(\"pluginAccounting.balanceSheet.sections.liability\");\n if (type === \"equity\") return t(\"pluginAccounting.balanceSheet.sections.equity\");\n return type;\n}\n\n// The server adds a synthetic \"Current period earnings\" row to the\n// Equity section so the B/S balances during the period (before\n// closing entries fold income/expense into Retained Earnings).\n// `_currentEarnings` is the sentinel accountCode emitted by the\n// server — see CURRENT_EARNINGS_ACCOUNT_CODE in\n// server/accounting/report.ts.\nconst CURRENT_EARNINGS_ACCOUNT_CODE = \"_currentEarnings\";\n\ninterface BSRow {\n accountCode: string;\n accountName: string;\n balance: number;\n}\n\nfunction isEarningsRow(row: BSRow): boolean {\n return row.accountCode === CURRENT_EARNINGS_ACCOUNT_CODE;\n}\n\nfunction rowName(row: BSRow): string {\n return isEarningsRow(row) ? t(\"pluginAccounting.balanceSheet.currentEarnings\") : row.accountName;\n}\n\n// Earnings row is synthetic (no underlying account on file), so it\n// can't be drilled into. Real-account rows route to the Ledger tab\n// pre-filtered to that account — same pattern as AccountsList.\nfunction onRowClick(row: BSRow): void {\n if (isEarningsRow(row)) return;\n emit(\"selectAccount\", row.accountCode);\n}\n\nfunction onKeyActivate(event: KeyboardEvent, row: BSRow): void {\n if (event.repeat) return;\n if (isEarningsRow(row)) return;\n emit(\"selectAccount\", row.accountCode);\n}\n\n// Mirrors the DateRangePicker pattern: hidden \"\" sentinel for the\n// \"no preset matches\" custom state, otherwise the dropdown shows\n// whichever shortcut produces the current period. Re-evaluates `now`\n// on every read so the labels stay correct across midnight without\n// any cache-invalidation plumbing.\ntype Shortcut = \"thisMonth\" | \"lastMonth\" | \"lastQuarter\" | \"lastYear\";\ntype SelectedShortcut = Shortcut | \"\";\n\nconst selectedShortcut = computed<SelectedShortcut>(() => {\n const { value } = period;\n const now = new Date();\n if (value === localMonthString(now)) return \"thisMonth\";\n if (value === previousMonthString(now)) return \"lastMonth\";\n if (value === lastMonthOfPreviousQuarterString(now)) return \"lastQuarter\";\n if (value === decemberOfPreviousYearString(now)) return \"lastYear\";\n return \"\";\n});\n\nfunction onShortcutChange(raw: string): void {\n const now = new Date();\n if (raw === \"thisMonth\") period.value = localMonthString(now);\n else if (raw === \"lastMonth\") period.value = previousMonthString(now);\n else if (raw === \"lastQuarter\") period.value = lastMonthOfPreviousQuarterString(now);\n else if (raw === \"lastYear\") period.value = decemberOfPreviousYearString(now);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n const result = await getBalanceSheet({ kind: \"month\", period: period.value }, props.bookId);\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n balanceSheet.value = null;\n return;\n }\n balanceSheet.value = result.data.balanceSheet;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nwatch(() => [props.bookId, props.version, period.value], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-balance-sheet\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.balanceSheet.shortcutLabel\") }}\n <select\n :value=\"selectedShortcut\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-bs-shortcut\"\n @change=\"onShortcutChange(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"\" hidden></option>\n <option value=\"thisMonth\">{{ t(\"pluginAccounting.balanceSheet.thisMonth\") }}</option>\n <option value=\"lastMonth\">{{ t(\"pluginAccounting.balanceSheet.lastMonth\") }}</option>\n <option value=\"lastQuarter\">{{ t(\"pluginAccounting.balanceSheet.lastQuarter\") }}</option>\n <option value=\"lastYear\">{{ t(\"pluginAccounting.balanceSheet.lastYear\") }}</option>\n </select>\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.balanceSheet.asOfLabel\") }}\n <input v-model=\"period\" type=\"month\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-bs-period\" />\n </label>\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"balanceSheet\">\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <section v-for=\"section in balanceSheet.sections\" :key=\"section.type\" class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ sectionLabel(section.type) }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in section.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100\"\n :class=\"\n isEarningsRow(row)\n ? 'italic text-gray-600'\n : 'hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400'\n \"\n :tabindex=\"isEarningsRow(row) ? -1 : 0\"\n :role=\"isEarningsRow(row) ? undefined : 'button'\"\n :aria-label=\"isEarningsRow(row) ? undefined : t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"isEarningsRow(row) ? undefined : `accounting-bs-row-${row.accountCode}`\"\n @click=\"onRowClick(row)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row)\"\n >\n <td class=\"py-1 px-1\">\n <span v-if=\"!isEarningsRow(row)\" class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ rowName(row) }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.balance) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(section.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n </div>\n <p :class=\"Math.abs(balanceSheet.imbalance) <= 0.01 ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-bs-imbalance\">\n {{ t(\"pluginAccounting.balanceSheet.imbalance\", { amount: formatAmount(balanceSheet.imbalance) }) }}\n </p>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { getBalanceSheet, type BalanceSheet } from \"../api\";\nimport {\n formatAmount as formatAmountWithCurrency,\n decemberOfPreviousYearString,\n lastMonthOfPreviousQuarterString,\n localMonthString,\n previousMonthString,\n} from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ bookId: string; currency: string; version: number }>();\nconst emit = defineEmits<{ selectAccount: [code: string] }>();\n\nconst period = ref(localMonthString());\nconst balanceSheet = ref<BalanceSheet | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nfunction sectionLabel(type: string): string {\n if (type === \"asset\") return t(\"pluginAccounting.balanceSheet.sections.asset\");\n if (type === \"liability\") return t(\"pluginAccounting.balanceSheet.sections.liability\");\n if (type === \"equity\") return t(\"pluginAccounting.balanceSheet.sections.equity\");\n return type;\n}\n\n// The server adds a synthetic \"Current period earnings\" row to the\n// Equity section so the B/S balances during the period (before\n// closing entries fold income/expense into Retained Earnings).\n// `_currentEarnings` is the sentinel accountCode emitted by the\n// server — see CURRENT_EARNINGS_ACCOUNT_CODE in\n// server/accounting/report.ts.\nconst CURRENT_EARNINGS_ACCOUNT_CODE = \"_currentEarnings\";\n\ninterface BSRow {\n accountCode: string;\n accountName: string;\n balance: number;\n}\n\nfunction isEarningsRow(row: BSRow): boolean {\n return row.accountCode === CURRENT_EARNINGS_ACCOUNT_CODE;\n}\n\nfunction rowName(row: BSRow): string {\n return isEarningsRow(row) ? t(\"pluginAccounting.balanceSheet.currentEarnings\") : row.accountName;\n}\n\n// Earnings row is synthetic (no underlying account on file), so it\n// can't be drilled into. Real-account rows route to the Ledger tab\n// pre-filtered to that account — same pattern as AccountsList.\nfunction onRowClick(row: BSRow): void {\n if (isEarningsRow(row)) return;\n emit(\"selectAccount\", row.accountCode);\n}\n\nfunction onKeyActivate(event: KeyboardEvent, row: BSRow): void {\n if (event.repeat) return;\n if (isEarningsRow(row)) return;\n emit(\"selectAccount\", row.accountCode);\n}\n\n// Mirrors the DateRangePicker pattern: hidden \"\" sentinel for the\n// \"no preset matches\" custom state, otherwise the dropdown shows\n// whichever shortcut produces the current period. Re-evaluates `now`\n// on every read so the labels stay correct across midnight without\n// any cache-invalidation plumbing.\ntype Shortcut = \"thisMonth\" | \"lastMonth\" | \"lastQuarter\" | \"lastYear\";\ntype SelectedShortcut = Shortcut | \"\";\n\nconst selectedShortcut = computed<SelectedShortcut>(() => {\n const { value } = period;\n const now = new Date();\n if (value === localMonthString(now)) return \"thisMonth\";\n if (value === previousMonthString(now)) return \"lastMonth\";\n if (value === lastMonthOfPreviousQuarterString(now)) return \"lastQuarter\";\n if (value === decemberOfPreviousYearString(now)) return \"lastYear\";\n return \"\";\n});\n\nfunction onShortcutChange(raw: string): void {\n const now = new Date();\n if (raw === \"thisMonth\") period.value = localMonthString(now);\n else if (raw === \"lastMonth\") period.value = previousMonthString(now);\n else if (raw === \"lastQuarter\") period.value = lastMonthOfPreviousQuarterString(now);\n else if (raw === \"lastYear\") period.value = decemberOfPreviousYearString(now);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n const result = await getBalanceSheet({ kind: \"month\", period: period.value }, props.bookId);\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n balanceSheet.value = null;\n return;\n }\n balanceSheet.value = result.data.balanceSheet;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nwatch(() => [props.bookId, props.version, period.value], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-profit-loss\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"profitLoss\">\n <section class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ t(\"pluginAccounting.profitLoss.income\") }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in profitLoss.income.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"`accounting-pl-row-${row.accountCode}`\"\n @click=\"onRowClick(row.accountCode)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n >\n <td class=\"py-1 px-1\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ row.accountName }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.amount) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(profitLoss.income.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n <section class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ t(\"pluginAccounting.profitLoss.expense\") }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in profitLoss.expense.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"`accounting-pl-row-${row.accountCode}`\"\n @click=\"onRowClick(row.accountCode)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n >\n <td class=\"py-1 px-1\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ row.accountName }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.amount) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(profitLoss.expense.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n <div class=\"flex justify-end items-center gap-2 text-sm font-semibold\" data-testid=\"accounting-pl-net\">\n <span>{{ t(\"pluginAccounting.profitLoss.netIncome\") }}</span>\n <span :class=\"profitLoss.netIncome >= 0 ? 'text-green-600' : 'text-red-500'\">{{ formatAmount(profitLoss.netIncome) }}</span>\n </div>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { getProfitLoss, type ProfitLoss } from \"../api\";\nimport { formatAmount as formatAmountWithCurrency, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n bookId: string;\n currency: string;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n}>();\n\nconst emit = defineEmits<{ selectAccount: [code: string] }>();\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\nfunction onRowClick(code: string): void {\n emit(\"selectAccount\", code);\n}\n\nfunction onKeyActivate(event: KeyboardEvent, code: string): void {\n if (event.repeat) return;\n emit(\"selectAccount\", code);\n}\n\n// Default = current fiscal year. Reset by the bookId/fiscalYearEnd\n// watcher below so switching books or changing the FY-end in\n// settings drops a stale custom range from the prior book.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\nconst profitLoss = ref<ProfitLoss | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n // P&L always sends a range. Empty-side gets a sentinel so \"All\"\n // (both empty) means \"every entry\" rather than an empty window.\n const fromBound = range.value.from || \"0000-01-01\";\n const toBound = range.value.to || \"9999-12-31\";\n const result = await getProfitLoss({ kind: \"range\", from: fromBound, to: toBound }, props.bookId);\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n profitLoss.value = null;\n return;\n }\n profitLoss.value = result.data.profitLoss;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\nwatch(() => [props.bookId, props.version, range.value.from, range.value.to], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-profit-loss\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"profitLoss\">\n <section class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ t(\"pluginAccounting.profitLoss.income\") }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in profitLoss.income.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"`accounting-pl-row-${row.accountCode}`\"\n @click=\"onRowClick(row.accountCode)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n >\n <td class=\"py-1 px-1\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ row.accountName }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.amount) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(profitLoss.income.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n <section class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ t(\"pluginAccounting.profitLoss.expense\") }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in profitLoss.expense.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"`accounting-pl-row-${row.accountCode}`\"\n @click=\"onRowClick(row.accountCode)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n >\n <td class=\"py-1 px-1\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ row.accountName }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.amount) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(profitLoss.expense.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n <div class=\"flex justify-end items-center gap-2 text-sm font-semibold\" data-testid=\"accounting-pl-net\">\n <span>{{ t(\"pluginAccounting.profitLoss.netIncome\") }}</span>\n <span :class=\"profitLoss.netIncome >= 0 ? 'text-green-600' : 'text-red-500'\">{{ formatAmount(profitLoss.netIncome) }}</span>\n </div>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { getProfitLoss, type ProfitLoss } from \"../api\";\nimport { formatAmount as formatAmountWithCurrency, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n bookId: string;\n currency: string;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n}>();\n\nconst emit = defineEmits<{ selectAccount: [code: string] }>();\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\nfunction onRowClick(code: string): void {\n emit(\"selectAccount\", code);\n}\n\nfunction onKeyActivate(event: KeyboardEvent, code: string): void {\n if (event.repeat) return;\n emit(\"selectAccount\", code);\n}\n\n// Default = current fiscal year. Reset by the bookId/fiscalYearEnd\n// watcher below so switching books or changing the FY-end in\n// settings drops a stale custom range from the prior book.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\nconst profitLoss = ref<ProfitLoss | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n // P&L always sends a range. Empty-side gets a sentinel so \"All\"\n // (both empty) means \"every entry\" rather than an empty window.\n const fromBound = range.value.from || \"0000-01-01\";\n const toBound = range.value.to || \"9999-12-31\";\n const result = await getProfitLoss({ kind: \"range\", from: fromBound, to: toBound }, props.bookId);\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n profitLoss.value = null;\n return;\n }\n profitLoss.value = result.data.profitLoss;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\nwatch(() => [props.bookId, props.version, range.value.from, range.value.to], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-4\" data-testid=\"accounting-settings\">\n <section class=\"border border-gray-200 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"pluginAccounting.settings.bookInfo\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.bookInfoExplain\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.nameLabel\") }}\n <input\n v-model=\"selectedName\"\n type=\"text\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-name\"\n :disabled=\"updating\"\n maxlength=\"200\"\n />\n </label>\n <dl class=\"text-xs text-gray-700 grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1\">\n <dt class=\"text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.currencyLabel\") }}</dt>\n <dd>{{ currency }}</dd>\n </dl>\n <label class=\"text-sm flex flex-col gap-1 mt-1\">\n {{ t(\"pluginAccounting.bookSwitcher.countryLabel\") }}\n <select\n v-model=\"selectedCountry\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-country\"\n :disabled=\"updating\"\n >\n <option value=\"\">{{ t(\"pluginAccounting.settings.countryUnset\") }}</option>\n <option v-for=\"opt in countryOptions\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <label class=\"text-sm flex flex-col gap-1 mt-1\">\n {{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndLabel\") }}\n <select\n v-model=\"selectedFiscalYearEnd\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-fiscal-year-end\"\n :disabled=\"updating\"\n >\n <option v-for=\"opt in fiscalYearEndOptions\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.fiscalYearEndExplain\") }}</p>\n <p v-if=\"updateOk\" class=\"text-xs text-green-600\" data-testid=\"accounting-settings-update-ok\">{{ updateOk }}</p>\n <p v-if=\"updateError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-update-error\">{{ updateError }}</p>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"updating || !hasPendingChanges\"\n data-testid=\"accounting-settings-save\"\n @click=\"onSaveBookInfo\"\n >\n {{ updating ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.saveChanges\") }}\n </button>\n </div>\n </section>\n <section class=\"border border-gray-200 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"pluginAccounting.settings.rebuild\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.rebuildExplain\") }}</p>\n <p v-if=\"rebuildOk\" class=\"text-xs text-green-600\" data-testid=\"accounting-settings-rebuild-ok\">{{ rebuildOk }}</p>\n <p v-if=\"rebuildError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-rebuild-error\">{{ rebuildError }}</p>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"rebuilding\"\n data-testid=\"accounting-settings-rebuild\"\n @click=\"onRebuild\"\n >\n {{ rebuilding ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.rebuild\") }}\n </button>\n </div>\n </section>\n <div v-if=\"!showAdvanced\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-settings-advanced\"\n @click=\"showAdvanced = true\"\n >\n <span class=\"material-icons text-base\">expand_more</span>\n <span>{{ t(\"pluginAccounting.settings.advanced\") }}</span>\n </button>\n </div>\n <section v-if=\"showAdvanced\" class=\"border border-red-300 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold text-red-700\">{{ t(\"pluginAccounting.settings.deleteBook\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.deleteBookExplain\") }}</p>\n <p v-if=\"deleteError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-delete-error\">{{ deleteError }}</p>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.settings.deleteBookConfirm\", { bookName: bookName }) }}\n <input v-model=\"confirmName\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-settings-delete-confirm\" />\n </label>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-red-600 hover:bg-red-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"confirmName !== bookName || deleting\"\n data-testid=\"accounting-settings-delete\"\n @click=\"onDelete\"\n >\n {{ deleting ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.deleteBookButton\") }}\n </button>\n </div>\n </section>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { deleteBook, rebuildSnapshots, updateBook } from \"../api\";\nimport {\n SUPPORTED_COUNTRY_CODES,\n isSupportedCountryCode,\n localizedCountryName,\n type SupportedCountryCode,\n DEFAULT_FISCAL_YEAR_END,\n FISCAL_YEAR_ENDS,\n resolveFiscalYearEnd,\n type FiscalYearEnd,\n} from \"../../shared\";\n\nconst { t, locale } = useI18n();\n\nconst props = defineProps<{\n bookId: string;\n bookName: string;\n currency: string;\n country?: SupportedCountryCode;\n fiscalYearEnd?: FiscalYearEnd;\n}>();\nconst emit = defineEmits<{ deleted: [bookName: string]; \"books-changed\": [] }>();\n\nconst rebuilding = ref(false);\nconst rebuildOk = ref<string | null>(null);\nconst rebuildError = ref<string | null>(null);\nconst deleting = ref(false);\nconst deleteError = ref<string | null>(null);\nconst confirmName = ref(\"\");\nconst updating = ref(false);\nconst updateOk = ref<string | null>(null);\nconst updateError = ref<string | null>(null);\nconst showAdvanced = ref(false);\nconst selectedName = ref<string>(props.bookName);\nconst selectedCountry = ref<string>(props.country ?? \"\");\n// Resolved at the boundary so the dropdown always shows a concrete\n// value — books without a `fiscalYearEnd` field on disk land here as\n// the default Q4 (matches the back-compat read policy).\nconst selectedFiscalYearEnd = ref<FiscalYearEnd>(props.fiscalYearEnd ?? DEFAULT_FISCAL_YEAR_END);\n\ninterface CountryOption {\n code: string;\n label: string;\n}\n\nconst countryOptions = computed<CountryOption[]>(() =>\n SUPPORTED_COUNTRY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCountryName(code, locale.value)}`,\n })),\n);\n\ninterface FiscalYearEndOption {\n value: FiscalYearEnd;\n label: string;\n}\n\nconst fiscalYearEndOptions = computed<FiscalYearEndOption[]>(() =>\n FISCAL_YEAR_ENDS.map((value) => ({\n value,\n label: t(`pluginAccounting.bookSwitcher.fiscalYearEnd${value}`),\n })),\n);\n\nconst hasPendingChanges = computed<boolean>(() => {\n // Compare against the trimmed value so a no-op edit (typing then\n // backspacing back to the original) doesn't keep the Save button\n // hot. Server-side validateUpdateBookInput rejects empty / whitespace\n // names with a 400 — the disabled binding below mirrors that contract\n // so the button can't fire a doomed request.\n const nameChanged = selectedName.value.trim() !== props.bookName;\n const nameValid = selectedName.value.trim().length > 0;\n const countryChanged = selectedCountry.value !== (props.country ?? \"\");\n const fiscalChanged = selectedFiscalYearEnd.value !== resolveFiscalYearEnd(props.fiscalYearEnd);\n return nameValid && (nameChanged || countryChanged || fiscalChanged);\n});\n\nasync function onRebuild(): Promise<void> {\n rebuilding.value = true;\n rebuildOk.value = null;\n rebuildError.value = null;\n try {\n const result = await rebuildSnapshots(props.bookId);\n if (!result.ok) {\n rebuildError.value = result.error;\n return;\n }\n rebuildOk.value = t(\"pluginAccounting.settings.rebuildOk\", { count: result.data.rebuilt.length });\n } finally {\n rebuilding.value = false;\n }\n}\n\nasync function onSaveBookInfo(): Promise<void> {\n if (updating.value) return;\n updating.value = true;\n updateOk.value = null;\n updateError.value = null;\n try {\n // The select v-model is a plain `string` (HTML form value); narrow\n // it back to the union before handing it to the API helper. The\n // empty string is the sentinel that clears the country server-side.\n const rawCountry = selectedCountry.value;\n const country: SupportedCountryCode | \"\" = rawCountry === \"\" || isSupportedCountryCode(rawCountry) ? rawCountry : \"\";\n const result = await updateBook({\n bookId: props.bookId,\n name: selectedName.value.trim(),\n country,\n fiscalYearEnd: selectedFiscalYearEnd.value,\n });\n if (!result.ok) {\n updateError.value = result.error;\n return;\n }\n updateOk.value = t(\"pluginAccounting.settings.updateOk\");\n emit(\"books-changed\");\n } finally {\n updating.value = false;\n }\n}\n\nasync function onDelete(): Promise<void> {\n if (deleting.value) return;\n deleting.value = true;\n deleteError.value = null;\n try {\n const result = await deleteBook(props.bookId);\n if (!result.ok) {\n deleteError.value = result.error;\n return;\n }\n emit(\"deleted\", props.bookName);\n emit(\"books-changed\");\n } finally {\n deleting.value = false;\n }\n}\n\n// Reset feedback / confirmation AND the dropdown selection when the\n// user navigates between books while this tab is open. Without the\n// `selectedCountry` reset, switching from book A (country=JP) to\n// book B (also country=JP) leaves a previously-typed unsaved value\n// staged on B — a save would then misattribute the edit.\nwatch(\n () => props.bookId,\n () => {\n rebuildOk.value = null;\n rebuildError.value = null;\n deleteError.value = null;\n confirmName.value = \"\";\n updateOk.value = null;\n updateError.value = null;\n selectedName.value = props.bookName;\n selectedCountry.value = props.country ?? \"\";\n selectedFiscalYearEnd.value = props.fiscalYearEnd ?? DEFAULT_FISCAL_YEAR_END;\n showAdvanced.value = false;\n },\n);\n\n// Follow external bookName updates — e.g. an LLM-driven updateBook in\n// another tab, or pubsub-driven refetch. Without this, an out-of-band\n// rename leaves a stale draft staged in the input.\nwatch(\n () => props.bookName,\n (next) => {\n selectedName.value = next;\n },\n);\n\nwatch(\n () => props.country,\n (next) => {\n selectedCountry.value = next ?? \"\";\n },\n);\n\nwatch(\n () => props.fiscalYearEnd,\n (next) => {\n selectedFiscalYearEnd.value = next ?? DEFAULT_FISCAL_YEAR_END;\n },\n);\n</script>\n","<template>\n <div class=\"flex flex-col gap-4\" data-testid=\"accounting-settings\">\n <section class=\"border border-gray-200 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"pluginAccounting.settings.bookInfo\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.bookInfoExplain\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.nameLabel\") }}\n <input\n v-model=\"selectedName\"\n type=\"text\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-name\"\n :disabled=\"updating\"\n maxlength=\"200\"\n />\n </label>\n <dl class=\"text-xs text-gray-700 grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1\">\n <dt class=\"text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.currencyLabel\") }}</dt>\n <dd>{{ currency }}</dd>\n </dl>\n <label class=\"text-sm flex flex-col gap-1 mt-1\">\n {{ t(\"pluginAccounting.bookSwitcher.countryLabel\") }}\n <select\n v-model=\"selectedCountry\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-country\"\n :disabled=\"updating\"\n >\n <option value=\"\">{{ t(\"pluginAccounting.settings.countryUnset\") }}</option>\n <option v-for=\"opt in countryOptions\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <label class=\"text-sm flex flex-col gap-1 mt-1\">\n {{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndLabel\") }}\n <select\n v-model=\"selectedFiscalYearEnd\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-fiscal-year-end\"\n :disabled=\"updating\"\n >\n <option v-for=\"opt in fiscalYearEndOptions\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.fiscalYearEndExplain\") }}</p>\n <p v-if=\"updateOk\" class=\"text-xs text-green-600\" data-testid=\"accounting-settings-update-ok\">{{ updateOk }}</p>\n <p v-if=\"updateError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-update-error\">{{ updateError }}</p>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"updating || !hasPendingChanges\"\n data-testid=\"accounting-settings-save\"\n @click=\"onSaveBookInfo\"\n >\n {{ updating ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.saveChanges\") }}\n </button>\n </div>\n </section>\n <section class=\"border border-gray-200 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"pluginAccounting.settings.rebuild\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.rebuildExplain\") }}</p>\n <p v-if=\"rebuildOk\" class=\"text-xs text-green-600\" data-testid=\"accounting-settings-rebuild-ok\">{{ rebuildOk }}</p>\n <p v-if=\"rebuildError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-rebuild-error\">{{ rebuildError }}</p>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"rebuilding\"\n data-testid=\"accounting-settings-rebuild\"\n @click=\"onRebuild\"\n >\n {{ rebuilding ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.rebuild\") }}\n </button>\n </div>\n </section>\n <div v-if=\"!showAdvanced\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-settings-advanced\"\n @click=\"showAdvanced = true\"\n >\n <span class=\"material-icons text-base\">expand_more</span>\n <span>{{ t(\"pluginAccounting.settings.advanced\") }}</span>\n </button>\n </div>\n <section v-if=\"showAdvanced\" class=\"border border-red-300 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold text-red-700\">{{ t(\"pluginAccounting.settings.deleteBook\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.deleteBookExplain\") }}</p>\n <p v-if=\"deleteError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-delete-error\">{{ deleteError }}</p>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.settings.deleteBookConfirm\", { bookName: bookName }) }}\n <input v-model=\"confirmName\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-settings-delete-confirm\" />\n </label>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-red-600 hover:bg-red-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"confirmName !== bookName || deleting\"\n data-testid=\"accounting-settings-delete\"\n @click=\"onDelete\"\n >\n {{ deleting ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.deleteBookButton\") }}\n </button>\n </div>\n </section>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { deleteBook, rebuildSnapshots, updateBook } from \"../api\";\nimport {\n SUPPORTED_COUNTRY_CODES,\n isSupportedCountryCode,\n localizedCountryName,\n type SupportedCountryCode,\n DEFAULT_FISCAL_YEAR_END,\n FISCAL_YEAR_ENDS,\n resolveFiscalYearEnd,\n type FiscalYearEnd,\n} from \"../../shared\";\n\nconst { t, locale } = useI18n();\n\nconst props = defineProps<{\n bookId: string;\n bookName: string;\n currency: string;\n country?: SupportedCountryCode;\n fiscalYearEnd?: FiscalYearEnd;\n}>();\nconst emit = defineEmits<{ deleted: [bookName: string]; \"books-changed\": [] }>();\n\nconst rebuilding = ref(false);\nconst rebuildOk = ref<string | null>(null);\nconst rebuildError = ref<string | null>(null);\nconst deleting = ref(false);\nconst deleteError = ref<string | null>(null);\nconst confirmName = ref(\"\");\nconst updating = ref(false);\nconst updateOk = ref<string | null>(null);\nconst updateError = ref<string | null>(null);\nconst showAdvanced = ref(false);\nconst selectedName = ref<string>(props.bookName);\nconst selectedCountry = ref<string>(props.country ?? \"\");\n// Resolved at the boundary so the dropdown always shows a concrete\n// value — books without a `fiscalYearEnd` field on disk land here as\n// the default Q4 (matches the back-compat read policy).\nconst selectedFiscalYearEnd = ref<FiscalYearEnd>(props.fiscalYearEnd ?? DEFAULT_FISCAL_YEAR_END);\n\ninterface CountryOption {\n code: string;\n label: string;\n}\n\nconst countryOptions = computed<CountryOption[]>(() =>\n SUPPORTED_COUNTRY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCountryName(code, locale.value)}`,\n })),\n);\n\ninterface FiscalYearEndOption {\n value: FiscalYearEnd;\n label: string;\n}\n\nconst fiscalYearEndOptions = computed<FiscalYearEndOption[]>(() =>\n FISCAL_YEAR_ENDS.map((value) => ({\n value,\n label: t(`pluginAccounting.bookSwitcher.fiscalYearEnd${value}`),\n })),\n);\n\nconst hasPendingChanges = computed<boolean>(() => {\n // Compare against the trimmed value so a no-op edit (typing then\n // backspacing back to the original) doesn't keep the Save button\n // hot. Server-side validateUpdateBookInput rejects empty / whitespace\n // names with a 400 — the disabled binding below mirrors that contract\n // so the button can't fire a doomed request.\n const nameChanged = selectedName.value.trim() !== props.bookName;\n const nameValid = selectedName.value.trim().length > 0;\n const countryChanged = selectedCountry.value !== (props.country ?? \"\");\n const fiscalChanged = selectedFiscalYearEnd.value !== resolveFiscalYearEnd(props.fiscalYearEnd);\n return nameValid && (nameChanged || countryChanged || fiscalChanged);\n});\n\nasync function onRebuild(): Promise<void> {\n rebuilding.value = true;\n rebuildOk.value = null;\n rebuildError.value = null;\n try {\n const result = await rebuildSnapshots(props.bookId);\n if (!result.ok) {\n rebuildError.value = result.error;\n return;\n }\n rebuildOk.value = t(\"pluginAccounting.settings.rebuildOk\", { count: result.data.rebuilt.length });\n } finally {\n rebuilding.value = false;\n }\n}\n\nasync function onSaveBookInfo(): Promise<void> {\n if (updating.value) return;\n updating.value = true;\n updateOk.value = null;\n updateError.value = null;\n try {\n // The select v-model is a plain `string` (HTML form value); narrow\n // it back to the union before handing it to the API helper. The\n // empty string is the sentinel that clears the country server-side.\n const rawCountry = selectedCountry.value;\n const country: SupportedCountryCode | \"\" = rawCountry === \"\" || isSupportedCountryCode(rawCountry) ? rawCountry : \"\";\n const result = await updateBook({\n bookId: props.bookId,\n name: selectedName.value.trim(),\n country,\n fiscalYearEnd: selectedFiscalYearEnd.value,\n });\n if (!result.ok) {\n updateError.value = result.error;\n return;\n }\n updateOk.value = t(\"pluginAccounting.settings.updateOk\");\n emit(\"books-changed\");\n } finally {\n updating.value = false;\n }\n}\n\nasync function onDelete(): Promise<void> {\n if (deleting.value) return;\n deleting.value = true;\n deleteError.value = null;\n try {\n const result = await deleteBook(props.bookId);\n if (!result.ok) {\n deleteError.value = result.error;\n return;\n }\n emit(\"deleted\", props.bookName);\n emit(\"books-changed\");\n } finally {\n deleting.value = false;\n }\n}\n\n// Reset feedback / confirmation AND the dropdown selection when the\n// user navigates between books while this tab is open. Without the\n// `selectedCountry` reset, switching from book A (country=JP) to\n// book B (also country=JP) leaves a previously-typed unsaved value\n// staged on B — a save would then misattribute the edit.\nwatch(\n () => props.bookId,\n () => {\n rebuildOk.value = null;\n rebuildError.value = null;\n deleteError.value = null;\n confirmName.value = \"\";\n updateOk.value = null;\n updateError.value = null;\n selectedName.value = props.bookName;\n selectedCountry.value = props.country ?? \"\";\n selectedFiscalYearEnd.value = props.fiscalYearEnd ?? DEFAULT_FISCAL_YEAR_END;\n showAdvanced.value = false;\n },\n);\n\n// Follow external bookName updates — e.g. an LLM-driven updateBook in\n// another tab, or pubsub-driven refetch. Without this, an out-of-band\n// rename leaves a stale draft staged in the input.\nwatch(\n () => props.bookName,\n (next) => {\n selectedName.value = next;\n },\n);\n\nwatch(\n () => props.country,\n (next) => {\n selectedCountry.value = next ?? \"\";\n },\n);\n\nwatch(\n () => props.fiscalYearEnd,\n (next) => {\n selectedFiscalYearEnd.value = next ?? DEFAULT_FISCAL_YEAR_END;\n },\n);\n</script>\n","// Subscribe to per-book accounting events.\n//\n// Returns a `version` ref that bumps every time the server publishes a\n// change for the given bookId — addEntries, voidEntry,\n// setOpeningBalances, upsertAccount, snapshot rebuild completion. View\n// components watch `version` to drive `refetch` calls.\n//\n// `bookId` is reactive: switching the active book in BookSwitcher\n// flips it; the composable unsubscribes from the old channel and\n// subscribes to the new one.\n//\n// `onPayload` is an optional fine-grained hook for callers that want to\n// inspect the event kind (e.g. show a \"rebuilding…\" indicator on\n// `kind: \"snapshots-rebuilding\"`).\n//\n// The raw pub/sub transport is host-injected via `hostSubscribe`\n// (see hostContext.ts) — the channel NAMES come from this package's\n// own `./shared` so publisher and subscriber stay in lockstep.\n\nimport { ref, watch, onUnmounted, type Ref } from \"vue\";\nimport { bookChannel, ACCOUNTING_BOOKS_CHANNEL, type BookChannelPayload } from \"../shared\";\nimport { hostSubscribe } from \"./hostContext\";\n\nexport interface UseAccountingChannelReturn {\n /** Bumps on every per-book event for the current bookId. Resets to\n * 0 when bookId changes. */\n version: Ref<number>;\n}\n\nexport function useAccountingChannel(bookId: Ref<string | null>, onPayload?: (payload: BookChannelPayload) => void): UseAccountingChannelReturn {\n const version = ref(0);\n let unsubscribe: (() => void) | null = null;\n\n function bind(nextBookId: string | null): void {\n unsubscribe?.();\n unsubscribe = null;\n version.value = 0;\n if (!nextBookId) return;\n unsubscribe = hostSubscribe(bookChannel(nextBookId), (data) => {\n const event = data as BookChannelPayload;\n version.value += 1;\n onPayload?.(event);\n });\n }\n\n watch(bookId, bind, { immediate: true });\n onUnmounted(() => {\n unsubscribe?.();\n unsubscribe = null;\n });\n return { version };\n}\n\n/** Subscribe to \"the list of books changed\" events. Use in\n * BookSwitcher.vue to refetch the dropdown contents when a sibling\n * tab adds / deletes a book. */\nexport function useAccountingBooksChannel(onChange: () => void): void {\n const unsubscribe = hostSubscribe(ACCOUNTING_BOOKS_CHANNEL, onChange);\n onUnmounted(() => unsubscribe());\n}\n","<template>\n <!-- Full <AccountingApp> mounted via the openBook tool result.\n Talks to /api/accounting directly for browse / form ops; only\n the entry gate (this mount) runs through the LLM. Pub/sub\n refetches keep multi-tab / sibling-window views in sync. -->\n <div class=\"h-full bg-white flex flex-col\" data-testid=\"accounting-app\">\n <NewBookForm v-if=\"showFirstRunForm\" first-run full-page @created=\"onFirstBookCreated\" />\n <template v-else>\n <header class=\"flex items-center justify-between gap-2 px-3 py-2 border-b border-gray-100 shrink-0\">\n <div class=\"flex items-center gap-2 min-w-0\">\n <span class=\"material-icons text-gray-600\">account_balance</span>\n <h2 class=\"text-lg font-semibold text-gray-800\">{{ t(\"pluginAccounting.title\") }}</h2>\n </div>\n <BookSwitcher\n v-if=\"initialLoadDone\"\n :model-value=\"activeBookId ?? ''\"\n :books=\"books\"\n @update:model-value=\"onBookSelected\"\n @books-changed=\"refetchBooks\"\n @book-created=\"onBookCreated\"\n />\n </header>\n <nav class=\"flex items-center gap-0.5 px-3 py-1.5 border-b border-gray-100 shrink-0 overflow-x-auto\" data-testid=\"accounting-tabs\">\n <button\n v-for=\"tab in visibleTabs\"\n :key=\"tab.key\"\n :class=\"[\n 'h-8 px-2.5 flex items-center gap-1 rounded text-sm whitespace-nowrap',\n deletedNoticeName !== null\n ? 'text-gray-400 cursor-not-allowed'\n : currentTab === tab.key\n ? 'bg-blue-50 text-blue-600 font-medium'\n : 'text-gray-600 hover:bg-gray-50',\n ]\"\n :data-testid=\"`accounting-tab-${tab.key}`\"\n :disabled=\"deletedNoticeName !== null\"\n @click=\"currentTab = tab.key\"\n >\n <span class=\"material-icons text-base\">{{ tab.icon }}</span>\n <span>{{ t(tab.labelKey) }}</span>\n </button>\n </nav>\n <main class=\"flex-1 overflow-auto p-4\">\n <div\n v-if=\"deletedNoticeName !== null\"\n class=\"text-center text-sm text-gray-600 flex flex-col gap-2 items-center justify-center h-full\"\n data-testid=\"accounting-deleted-notice\"\n >\n <span class=\"material-icons text-gray-400\" style=\"font-size: 48px\">delete_outline</span>\n <p class=\"font-medium\" data-testid=\"accounting-deleted-notice-title\">\n {{ t(\"pluginAccounting.deletedNotice.title\", { bookName: deletedNoticeName }) }}\n </p>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.deletedNotice.body\") }}</p>\n </div>\n <p v-else-if=\"loadingBooks && !initialLoadDone\" class=\"text-sm text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"bookLoadError\" class=\"text-sm text-red-500\" data-testid=\"accounting-load-error\">\n {{ t(\"pluginAccounting.common.error\", { error: bookLoadError }) }}\n </p>\n <p v-else-if=\"!activeBookId\" class=\"text-sm text-gray-500\" data-testid=\"accounting-no-book\">{{ t(\"pluginAccounting.noBook\") }}</p>\n <template v-else-if=\"activeBookId\">\n <JournalList\n v-if=\"currentTab === 'journal'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :country=\"activeCountry\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n :preselect-entry-id=\"journalPreselectEntryId\"\n @edit-opening=\"currentTab = 'opening'\"\n @preselect-consumed=\"journalPreselectEntryId = undefined\"\n />\n <OpeningBalancesForm\n v-else-if=\"currentTab === 'opening'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n @submitted=\"onEntrySubmitted\"\n />\n <AccountsList v-else-if=\"currentTab === 'accounts'\" :book-id=\"activeBookId\" :accounts=\"accounts\" @select-account=\"onAccountSelected\" />\n <Ledger\n v-else-if=\"currentTab === 'ledger'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n :preselect-account-code=\"ledgerPreselectAccountCode\"\n />\n <BalanceSheet\n v-else-if=\"currentTab === 'balanceSheet'\"\n :book-id=\"activeBookId\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n @select-account=\"onAccountSelected\"\n />\n <ProfitLoss\n v-else-if=\"currentTab === 'profitLoss'\"\n :book-id=\"activeBookId\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n @select-account=\"onAccountSelected\"\n />\n <BookSettings\n v-else-if=\"currentTab === 'settings'\"\n :book-id=\"activeBookId\"\n :book-name=\"activeBookName\"\n :currency=\"activeCurrency\"\n :country=\"activeCountry\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n @deleted=\"onBookDeleted\"\n @books-changed=\"refetchBooks\"\n />\n </template>\n </main>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport BookSwitcher from \"./components/BookSwitcher.vue\";\nimport NewBookForm from \"./components/NewBookForm.vue\";\nimport JournalList from \"./components/JournalList.vue\";\nimport OpeningBalancesForm from \"./components/OpeningBalancesForm.vue\";\nimport AccountsList from \"./components/AccountsList.vue\";\nimport Ledger from \"./components/Ledger.vue\";\nimport BalanceSheet from \"./components/BalanceSheet.vue\";\nimport ProfitLoss from \"./components/ProfitLoss.vue\";\nimport BookSettings from \"./components/BookSettings.vue\";\nimport { getOpeningBalances, getAccounts, getBooks, type Account, type BookSummary } from \"./api\";\nimport { ACCOUNTING_ACTIONS } from \"../shared\";\nimport { useAccountingChannel, useAccountingBooksChannel } from \"./useAccountingChannel\";\nimport { errorMessage } from \"../shared/errors\";\n\nconst { t } = useI18n();\n\ninterface AccountingAppPayload {\n kind?: string;\n bookId?: string;\n initialTab?: string;\n /** Dispatch verb stamped onto every accounting tool-result envelope\n * (server/api/routes/accounting.ts dispatch()). We read it here to\n * pick the canvas tab + journal preselect for each PREVIEW action. */\n action?: string;\n /** Present on `addEntries` envelopes — the freshly-built journal\n * entries returned by the service. Each carries a server-stamped\n * `id` we use to highlight the row in JournalList. */\n entries?: { id?: string }[];\n /** Present on `voidEntry` envelopes — the kind=\"void-marker\" row\n * posted alongside the reversing entry. We surface this row (not\n * the reverseEntry) because the marker is the visual \"this entry\n * was voided here\" indicator the user is looking for. */\n markerEntry?: { id?: string };\n}\n\nconst props = defineProps<{ selectedResult?: ToolResultComplete<AccountingAppPayload, AccountingAppPayload> }>();\n\nconst TAB_KEYS = [\"journal\", \"opening\", \"accounts\", \"ledger\", \"balanceSheet\", \"profitLoss\", \"settings\"] as const;\ntype TabKey = (typeof TAB_KEYS)[number];\n\ninterface TabDef {\n key: TabKey;\n icon: string;\n labelKey: string;\n}\n\nconst TABS: readonly TabDef[] = [\n { key: \"journal\", icon: \"list\", labelKey: \"pluginAccounting.tabs.journal\" },\n { key: \"opening\", icon: \"play_arrow\", labelKey: \"pluginAccounting.tabs.opening\" },\n { key: \"accounts\", icon: \"list_alt\", labelKey: \"pluginAccounting.tabs.accounts\" },\n { key: \"ledger\", icon: \"menu_book\", labelKey: \"pluginAccounting.tabs.ledger\" },\n { key: \"balanceSheet\", icon: \"balance\", labelKey: \"pluginAccounting.tabs.balanceSheet\" },\n { key: \"profitLoss\", icon: \"trending_up\", labelKey: \"pluginAccounting.tabs.profitLoss\" },\n { key: \"settings\", icon: \"settings\", labelKey: \"pluginAccounting.tabs.settings\" },\n];\n\nfunction isTabKey(value: string | undefined): value is TabKey {\n return typeof value === \"string\" && (TAB_KEYS as readonly string[]).includes(value);\n}\n\nconst initialPayload = computed<AccountingAppPayload>(() => props.selectedResult?.data ?? props.selectedResult?.jsonData ?? {});\nconst initialTab = computed<TabKey>(() => (isTabKey(initialPayload.value.initialTab) ? initialPayload.value.initialTab : \"journal\"));\n\nconst currentTab = ref<TabKey>(initialTab.value);\nconst books = ref<BookSummary[]>([]);\nconst activeBookId = ref<string | null>(null);\nconst accounts = ref<Account[]>([]);\nconst loadingBooks = ref(true);\n// Sticky once the first books fetch lands. Lets the BookSwitcher stay\n// mounted across subsequent refetches (delete, create, pubsub-driven)\n// so the user sees the dropdown smoothly update its selection rather\n// than having the whole component flash in and out via `v-if`.\nconst initialLoadDone = ref(false);\n// First-run flow: when the user opens the app on a fresh\n// workspace (zero books), we render NewBookForm in full-page\n// mode in place of the regular chrome (header + tabs + main),\n// so the user MUST pick a name + currency before proceeding —\n// no popup, no dismiss. Distinct from the modal opened via\n// BookSwitcher's \"+ New book\" sentinel option, which reuses the\n// same component but with the overlay layout.\nconst showFirstRunForm = ref(false);\nconst firstRunHandled = ref(false);\n// Distinct from \"books is empty\" so we don't show the \"+ New\n// book\" CTA when the real problem is a transport / server failure\n// fetching the list.\nconst bookLoadError = ref<string | null>(null);\n// Tracks whether the active book has an opening entry on file.\n// `null` = unknown / loading; the gate only activates on an\n// explicit `false` so we don't disable tabs during the cold load\n// while the first getOpeningBalances request is still in flight.\nconst hasOpening = ref<boolean | null>(null);\n// Date of the active book's opening entry, plumbed down to the\n// DateRangePicker via the children so \"All\" can resolve to\n// (openingDate → today). `undefined` while loading / for books\n// without an opening on file (the opening gate prevents any tab\n// that would care from being shown in that state).\nconst activeOpeningDate = ref<string | undefined>(undefined);\n// Special \"you just deleted this book\" UI state. When set to a\n// non-null book name, the entire tab strip + main content are\n// replaced by an explicit \"<book> has been deleted — pick another\n// from the dropdown\" panel. Cleared the moment the user picks a\n// book from the BookSwitcher (or creates a new one). The View does\n// NOT auto-route to books[0] because that hides the fact that the\n// previously-active book is gone — issue #1126 (1) calls this\n// experience \"very confusing\".\nconst deletedNoticeName = ref<string | null>(null);\n\nconst activeBook = computed(() => books.value.find((book) => book.id === activeBookId.value) ?? null);\nconst activeBookName = computed(() => activeBook.value?.name ?? \"\");\nconst activeCurrency = computed(() => activeBook.value?.currency ?? \"USD\");\nconst activeCountry = computed(() => activeBook.value?.country);\nconst activeFiscalYearEnd = computed(() => activeBook.value?.fiscalYearEnd);\n\n// Single sync signal: every mutating service function publishes on\n// the accounting book channel after its write, so the sender's own\n// SSE round-trip drives the table/report refetch. No parallel\n// localVersion bump — it only ever fired the same watchers a second\n// time in the same tick.\nconst { version: bookVersion } = useAccountingChannel(activeBookId);\nuseAccountingBooksChannel(() => void refetchBooks());\n\nfunction pickInitialBookId(): string | null {\n // Priority: explicit `initialPayload.bookId` (carried in the\n // tool-result envelope by openBook / createBook / addEntries / …) →\n // first book in the list → null (empty workspace). The candidate\n // is validated against the live book list so a stale id from a\n // deleted book doesn't poison the View.\n if (books.value.length === 0) return null;\n const requested = initialPayload.value.bookId;\n if (requested && books.value.some((book) => book.id === requested)) return requested;\n return books.value[0].id;\n}\n\nasync function refetchBooks(): Promise<void> {\n loadingBooks.value = true;\n bookLoadError.value = null;\n // Capture the current active book BEFORE the fetch so we can\n // surface its name in the deleted-notice panel if the fetch\n // reveals it's gone. Without this snapshot, an SSE-driven refetch\n // racing ahead of the local deleteBook HTTP response would resolve\n // with `activeBook` already pointing at a now-stale entry.\n const previousActive = activeBook.value;\n try {\n const result = await getBooks();\n if (!result.ok) {\n // Surface load failures as a distinct error state so the user\n // doesn't see \"No books yet\" (and the auto-open modal) when\n // the real cause is a transport / server problem.\n bookLoadError.value = result.error;\n return;\n }\n books.value = result.data.books;\n // Sticky-true once a successful fetch lands. Setting it here (in\n // the success branch) rather than in `finally` means a first-load\n // transport / 5xx failure leaves BookSwitcher hidden — the user\n // sees only the `accounting-load-error` message rather than an\n // empty dropdown with a live \"+ New book\" path that has nothing\n // to fall back on.\n initialLoadDone.value = true;\n // While the deleted-notice panel is already up, leave activeBookId\n // alone — the user has to pick the next book themselves via\n // the BookSwitcher (and onBookSelected then clears the notice).\n // Otherwise pickInitialBookId would silently re-select books[0]\n // and undo the entire deletion-state UX.\n if (deletedNoticeName.value === null) {\n const stillExists = activeBookId.value !== null && books.value.some((book) => book.id === activeBookId.value);\n if (!stillExists) {\n // The active book just disappeared from the server's list.\n // Race-source possibilities, all converging here:\n // • local deleteBook → publishBooksChanged → SSE arrives\n // before the HTTP response handler can call onBookDeleted;\n // • a sibling tab / LLM tool deleted the book out-of-band.\n // In all cases the user needs to know what happened — show\n // the deleted-notice panel keyed off the previously-active\n // book's name, rather than silently snapping to books[0].\n // Falls back to the previous pickInitialBookId behaviour only\n // when there was no active book to lose (cold start).\n if (previousActive) {\n activeBookId.value = null;\n deletedNoticeName.value = previousActive.name;\n } else {\n activeBookId.value = pickInitialBookId();\n }\n }\n }\n // Auto-open the New Book modal exactly once on first arrival\n // when the workspace is empty. After that, the user can still\n // open it manually via the \"+ New book\" button.\n if (!firstRunHandled.value && books.value.length === 0) {\n firstRunHandled.value = true;\n showFirstRunForm.value = true;\n }\n } catch (err) {\n bookLoadError.value = errorMessage(err);\n } finally {\n loadingBooks.value = false;\n }\n}\n\nasync function onFirstBookCreated(book: BookSummary): Promise<void> {\n showFirstRunForm.value = false;\n await refetchBooks();\n activeBookId.value = book.id;\n}\n\n// Optimistically insert the new book and set the selection\n// BEFORE the refetch round-trip. Two reasons this beats the\n// previous await-refetch-then-select shape:\n// 1. The pubsub handler `useAccountingBooksChannel` fires its\n// own concurrent `refetchBooks` the instant the server\n// publishes books-changed. With await-then-select, that\n// concurrent refetch's stillExists guard reads the OLD\n// activeBookId (we haven't updated it yet) and — because\n// OLD is still in the books list — leaves the selection\n// pointing at OLD. Our update lands AFTER, but BookSwitcher\n// remounts under `v-if=\"!loadingBooks\"` mid-flight, so the\n// user sees the dropdown stick on OLD.\n// 2. With activeBookId already set to NEW and books pre-\n// populated to include NEW, every concurrent refetch's\n// stillExists check passes for NEW and leaves the selection\n// alone — order-independent by construction.\nasync function onBookCreated(book: BookSummary): Promise<void> {\n if (!books.value.some((existing) => existing.id === book.id)) {\n books.value = [...books.value, book];\n }\n activeBookId.value = book.id;\n // Creating a new book is also the \"exit\" out of the deleted-notice\n // panel — the user explicitly chose the new book, so re-enable the\n // tab strip and let the opening-gate watcher route them to Opening.\n deletedNoticeName.value = null;\n // currentTab may be on \"settings\" (the user opened the create\n // modal from there) — reset to journal so the openingGateActive\n // watcher's \"if (currentTab.value === 'opening') return\" gate\n // doesn't strand the user on settings while the gate is active.\n currentTab.value = \"journal\";\n await refetchBooks();\n}\n\nasync function refetchAccounts(): Promise<void> {\n if (!activeBookId.value) {\n accounts.value = [];\n return;\n }\n const result = await getAccounts(activeBookId.value);\n if (!result.ok) return;\n accounts.value = result.data.accounts;\n}\n\nasync function refetchOpening(): Promise<void> {\n if (!activeBookId.value) {\n hasOpening.value = null;\n activeOpeningDate.value = undefined;\n return;\n }\n const result = await getOpeningBalances(activeBookId.value);\n if (!result.ok) return;\n hasOpening.value = result.data.opening !== null;\n activeOpeningDate.value = result.data.opening?.date;\n}\n\n// A book without an opening on file is in \"gated\" mode: the user\n// must save an opening (empty is fine — see OpeningBalancesForm)\n// before journal / report tabs unlock. Settings stays accessible\n// so the user can delete the book if they don't want to proceed.\nconst openingGateActive = computed(() => activeBookId.value !== null && hasOpening.value === false);\n\n// Gated → only Opening + Settings render in the strip. Ungated →\n// Opening hides itself; users reach the form via the Edit button\n// on the active opening row in the journal, which transiently\n// switches `currentTab` to \"opening\" (kept visible while there).\nconst visibleTabs = computed<readonly TabDef[]>(() => {\n if (openingGateActive.value) return TABS.filter((tab) => tab.key === \"opening\" || tab.key === \"settings\");\n return TABS.filter((tab) => tab.key !== \"opening\" || currentTab.value === \"opening\");\n});\n\nfunction onBookSelected(bookId: string): void {\n activeBookId.value = bookId;\n // Picking a book from the dropdown is the explicit \"I'm done\n // looking at the deleted notice\" exit. Clear it so the tab strip\n // re-enables for the freshly selected book.\n deletedNoticeName.value = null;\n}\n\n// Entry id to surface in JournalList after an `addEntries` tool\n// result lands — the LLM just posted a journal entry and we want\n// the user's eye on the new row. Multi-entry batches highlight the\n// LAST entry only (matches the \"you ended up here\" intent of a\n// scroll-to-cursor).\nconst journalPreselectEntryId = ref<string | undefined>(undefined);\n\n// Account preselected by the Accounts tab → click handoff. Cleared\n// once the user picks a different account from the Ledger's own\n// dropdown so a stale preselection doesn't override later edits.\nconst ledgerPreselectAccountCode = ref<string | undefined>(undefined);\n\nfunction onAccountSelected(code: string): void {\n // Force the ref to a fresh value even when the user clicks the\n // same account a second time — the Ledger's `watch(preselect…)`\n // ignores no-op updates, so we'd otherwise leave the user on a\n // stale Ledger state if they navigated away and clicked back.\n ledgerPreselectAccountCode.value = undefined;\n Promise.resolve().then(() => {\n ledgerPreselectAccountCode.value = code;\n });\n currentTab.value = \"ledger\";\n}\n\nfunction onEntrySubmitted(): void {\n // After saving an opening, switch to the journal so the user\n // immediately sees the unlocked tabs. The server-side\n // publishBookChange triggers the bookVersion watcher over SSE,\n // which refetches hasOpening, so the gate auto-lifts shortly after\n // the tab switch — no manual unlock needed here. Normal entries\n // are now posted from the inline form inside JournalList; that\n // form drives its own dismissal and the journal repaints in\n // place.\n if (currentTab.value === \"opening\") {\n currentTab.value = \"journal\";\n }\n}\n\nasync function onBookDeleted(deletedName: string): Promise<void> {\n // Reset the tab BEFORE awaiting so a fast delete-then-create\n // can't race: if the new book's gate engages while we're still\n // awaiting refetchBooks, the gate watcher needs to see a\n // non-\"settings\" currentTab to route the user to Opening.\n currentTab.value = \"journal\";\n // Drop the active selection so refetchBooks doesn't auto-pick\n // books[0] — the user should see the deleted-notice panel and\n // explicitly switch via the BookSwitcher rather than be silently\n // moved to a different book (issue #1126).\n activeBookId.value = null;\n deletedNoticeName.value = deletedName;\n await refetchBooks();\n}\n\n// Refetch the chart of accounts whenever the active book changes\n// or any pub/sub / child action bumps bookVersion (e.g. an\n// upsertAccount from the Manage Accounts modal, or an LLM-driven\n// upsert in another tab). The list is small JSON; the cost of\n// over-fetching on entry / void / opening events is negligible\n// against the staleness bug it removes.\nwatch(\n () => [activeBookId.value, bookVersion.value],\n () => {\n if (activeBookId.value) void refetchAccounts();\n },\n { immediate: true },\n);\n\n// Drop any leftover Accounts → Ledger preselection when the active\n// book changes. Without this, picking account \"1000\" in book A's\n// Accounts tab and then switching to book B would carry the hint\n// across, so book B's Ledger would auto-select \"1000\" (which may\n// be an unrelated account in B's chart, or absent entirely).\nwatch(activeBookId, () => {\n ledgerPreselectAccountCode.value = undefined;\n});\n\n// Stash a target bookId that we want to land on but haven't been\n// able to apply yet (book not in `books` at the moment the\n// tool-result fired). Cleared as soon as the books list catches up.\nconst pendingTargetBookId = ref<string | null>(null);\n\nfunction applyTargetBookId(target: string): void {\n if (books.value.some((book) => book.id === target)) {\n activeBookId.value = target;\n pendingTargetBookId.value = null;\n return;\n }\n pendingTargetBookId.value = target;\n}\n\n// When the selected tool-result changes (user clicks a different\n// preview card in the sidebar), follow the new result's bookId so\n// the canvas lands on the book that action just touched. Skipped\n// when the new result has no bookId (silent reads / actions that\n// don't carry one). When the target isn't in `books` yet — common\n// race after a fresh `createBook → openBook(bookId)` handoff where\n// the result envelope arrives before refetchBooks completes — the\n// id is stashed and applied by the books watcher below as soon as\n// the list catches up.\nwatch(\n () => initialPayload.value.bookId,\n (next) => {\n if (!next) return;\n applyTargetBookId(next);\n },\n);\n\n// Drains the pending target once `books` includes it (typically\n// after a pub/sub-driven refetch resolves the createBook write).\n// No-op when nothing is pending or the target is still missing.\nwatch(books, () => {\n const pending = pendingTargetBookId.value;\n if (pending) applyTargetBookId(pending);\n});\n\n// Map a PREVIEW action to the canvas tab the user should land on.\n// Honours an explicit `initialTab` from the envelope (the LLM's\n// stated intent) over the action-default below — only `openBook`\n// currently ships initialTab, but the override is plugin-wide.\n//\n// The `balanceSheet` default for openBook / createBook /\n// setOpeningBalances assumes the book has an opening on file. For a\n// fresh book without one, the existing `openingGateActive` watcher\n// redirects to \"opening\" — we don't try to short-circuit that here\n// because hasOpening hasn't necessarily resolved when this runs.\nfunction pickTabForAction(payload: AccountingAppPayload): TabKey | null {\n if (isTabKey(payload.initialTab)) return payload.initialTab;\n switch (payload.action) {\n case ACCOUNTING_ACTIONS.addEntries:\n case ACCOUNTING_ACTIONS.voidEntry:\n return \"journal\";\n case ACCOUNTING_ACTIONS.upsertAccount:\n return \"accounts\";\n case ACCOUNTING_ACTIONS.updateBook:\n return \"settings\";\n case ACCOUNTING_ACTIONS.openBook:\n case ACCOUNTING_ACTIONS.createBook:\n case ACCOUNTING_ACTIONS.setOpeningBalances:\n return \"balanceSheet\";\n default:\n return null;\n }\n}\n\n// For tool results that should auto-expand a row in JournalList,\n// derive the entry id from the action's payload. addEntries picks\n// the LAST entry in the batch (\"you ended up here\" cursor); voidEntry\n// picks the void-MARKER (the visual \"voided here\" indicator), not\n// the reversing entry.\nfunction pickJournalPreselectId(payload: AccountingAppPayload): string | undefined {\n if (payload.action === ACCOUNTING_ACTIONS.addEntries) {\n const entries = Array.isArray(payload.entries) ? payload.entries : [];\n return entries[entries.length - 1]?.id;\n }\n if (payload.action === ACCOUNTING_ACTIONS.voidEntry) {\n return payload.markerEntry?.id;\n }\n return undefined;\n}\n\n// Drive canvas tab + journal preselect from the active tool-result\n// envelope. The route handler stamps `data: { action, bookId, … }`\n// onto every PREVIEW action's response (server/api/routes/\n// accounting.ts dispatch + PREVIEW_ACTIONS). `immediate: true` so a\n// cold open with the result already selected (e.g., reload after\n// the LLM dispatched) routes to the right surface too.\n//\n// Preselect is *always* assigned (not `if (preselect)`) so a\n// subsequent non-addEntries/voidEntry tool result clears any stale\n// id left over from a prior addEntries the user has already seen —\n// otherwise the next JournalList remount would replay it. The child\n// also emits `preselectConsumed` after expanding for the same\n// reason.\nwatch(\n () => initialPayload.value,\n (payload) => {\n const targetTab = pickTabForAction(payload);\n if (targetTab) currentTab.value = targetTab;\n journalPreselectEntryId.value = pickJournalPreselectId(payload);\n },\n { immediate: true },\n);\n\n// Drop the journal preselect on a real book SWITCH — leftover ids\n// from the prior book don't exist in the new one. The cold-load\n// transition (null → bookId) doesn't qualify: refetchBooks resolves\n// activeBookId asynchronously and would otherwise clobber a\n// preselect the addEntries watcher just set on initial mount.\nwatch(activeBookId, (_next, prev) => {\n if (!prev) return;\n journalPreselectEntryId.value = undefined;\n});\n\n// Refetch the opening status whenever the active book changes or\n// any pub/sub / child action bumps bookVersion (e.g. an opening\n// got saved or voided). Clears hasOpening when the book goes null\n// so a stale \"true\" doesn't carry over between books.\nwatch(\n () => [activeBookId.value, bookVersion.value],\n () => void refetchOpening(),\n { immediate: true },\n);\n\n// Force-route to the Opening tab whenever the gate engages.\n// Other tabs are hidden from the strip while gated, but this\n// watcher handles the programmatic case where currentTab still\n// points at a now-hidden tab (book switch, initial mount with a\n// no-opening book, LLM-supplied initialTab pointing at a gated\n// tab, or fresh-book creation right after deleting from the\n// settings tab) — without it, `<main>` would render nothing or\n// the user would be stranded on the prior book's settings view.\n// We don't exempt \"settings\" here: the user can still click back\n// to it from the (gated) tab strip if they want to delete the\n// new book instead of setting it up.\nwatch(openingGateActive, (active) => {\n if (!active) return;\n if (currentTab.value === \"opening\") return;\n currentTab.value = \"opening\";\n});\n\nvoid refetchBooks();\n</script>\n","<template>\n <!-- Full <AccountingApp> mounted via the openBook tool result.\n Talks to /api/accounting directly for browse / form ops; only\n the entry gate (this mount) runs through the LLM. Pub/sub\n refetches keep multi-tab / sibling-window views in sync. -->\n <div class=\"h-full bg-white flex flex-col\" data-testid=\"accounting-app\">\n <NewBookForm v-if=\"showFirstRunForm\" first-run full-page @created=\"onFirstBookCreated\" />\n <template v-else>\n <header class=\"flex items-center justify-between gap-2 px-3 py-2 border-b border-gray-100 shrink-0\">\n <div class=\"flex items-center gap-2 min-w-0\">\n <span class=\"material-icons text-gray-600\">account_balance</span>\n <h2 class=\"text-lg font-semibold text-gray-800\">{{ t(\"pluginAccounting.title\") }}</h2>\n </div>\n <BookSwitcher\n v-if=\"initialLoadDone\"\n :model-value=\"activeBookId ?? ''\"\n :books=\"books\"\n @update:model-value=\"onBookSelected\"\n @books-changed=\"refetchBooks\"\n @book-created=\"onBookCreated\"\n />\n </header>\n <nav class=\"flex items-center gap-0.5 px-3 py-1.5 border-b border-gray-100 shrink-0 overflow-x-auto\" data-testid=\"accounting-tabs\">\n <button\n v-for=\"tab in visibleTabs\"\n :key=\"tab.key\"\n :class=\"[\n 'h-8 px-2.5 flex items-center gap-1 rounded text-sm whitespace-nowrap',\n deletedNoticeName !== null\n ? 'text-gray-400 cursor-not-allowed'\n : currentTab === tab.key\n ? 'bg-blue-50 text-blue-600 font-medium'\n : 'text-gray-600 hover:bg-gray-50',\n ]\"\n :data-testid=\"`accounting-tab-${tab.key}`\"\n :disabled=\"deletedNoticeName !== null\"\n @click=\"currentTab = tab.key\"\n >\n <span class=\"material-icons text-base\">{{ tab.icon }}</span>\n <span>{{ t(tab.labelKey) }}</span>\n </button>\n </nav>\n <main class=\"flex-1 overflow-auto p-4\">\n <div\n v-if=\"deletedNoticeName !== null\"\n class=\"text-center text-sm text-gray-600 flex flex-col gap-2 items-center justify-center h-full\"\n data-testid=\"accounting-deleted-notice\"\n >\n <span class=\"material-icons text-gray-400\" style=\"font-size: 48px\">delete_outline</span>\n <p class=\"font-medium\" data-testid=\"accounting-deleted-notice-title\">\n {{ t(\"pluginAccounting.deletedNotice.title\", { bookName: deletedNoticeName }) }}\n </p>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.deletedNotice.body\") }}</p>\n </div>\n <p v-else-if=\"loadingBooks && !initialLoadDone\" class=\"text-sm text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"bookLoadError\" class=\"text-sm text-red-500\" data-testid=\"accounting-load-error\">\n {{ t(\"pluginAccounting.common.error\", { error: bookLoadError }) }}\n </p>\n <p v-else-if=\"!activeBookId\" class=\"text-sm text-gray-500\" data-testid=\"accounting-no-book\">{{ t(\"pluginAccounting.noBook\") }}</p>\n <template v-else-if=\"activeBookId\">\n <JournalList\n v-if=\"currentTab === 'journal'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :country=\"activeCountry\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n :preselect-entry-id=\"journalPreselectEntryId\"\n @edit-opening=\"currentTab = 'opening'\"\n @preselect-consumed=\"journalPreselectEntryId = undefined\"\n />\n <OpeningBalancesForm\n v-else-if=\"currentTab === 'opening'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n @submitted=\"onEntrySubmitted\"\n />\n <AccountsList v-else-if=\"currentTab === 'accounts'\" :book-id=\"activeBookId\" :accounts=\"accounts\" @select-account=\"onAccountSelected\" />\n <Ledger\n v-else-if=\"currentTab === 'ledger'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n :preselect-account-code=\"ledgerPreselectAccountCode\"\n />\n <BalanceSheet\n v-else-if=\"currentTab === 'balanceSheet'\"\n :book-id=\"activeBookId\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n @select-account=\"onAccountSelected\"\n />\n <ProfitLoss\n v-else-if=\"currentTab === 'profitLoss'\"\n :book-id=\"activeBookId\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n @select-account=\"onAccountSelected\"\n />\n <BookSettings\n v-else-if=\"currentTab === 'settings'\"\n :book-id=\"activeBookId\"\n :book-name=\"activeBookName\"\n :currency=\"activeCurrency\"\n :country=\"activeCountry\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n @deleted=\"onBookDeleted\"\n @books-changed=\"refetchBooks\"\n />\n </template>\n </main>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport BookSwitcher from \"./components/BookSwitcher.vue\";\nimport NewBookForm from \"./components/NewBookForm.vue\";\nimport JournalList from \"./components/JournalList.vue\";\nimport OpeningBalancesForm from \"./components/OpeningBalancesForm.vue\";\nimport AccountsList from \"./components/AccountsList.vue\";\nimport Ledger from \"./components/Ledger.vue\";\nimport BalanceSheet from \"./components/BalanceSheet.vue\";\nimport ProfitLoss from \"./components/ProfitLoss.vue\";\nimport BookSettings from \"./components/BookSettings.vue\";\nimport { getOpeningBalances, getAccounts, getBooks, type Account, type BookSummary } from \"./api\";\nimport { ACCOUNTING_ACTIONS } from \"../shared\";\nimport { useAccountingChannel, useAccountingBooksChannel } from \"./useAccountingChannel\";\nimport { errorMessage } from \"../shared/errors\";\n\nconst { t } = useI18n();\n\ninterface AccountingAppPayload {\n kind?: string;\n bookId?: string;\n initialTab?: string;\n /** Dispatch verb stamped onto every accounting tool-result envelope\n * (server/api/routes/accounting.ts dispatch()). We read it here to\n * pick the canvas tab + journal preselect for each PREVIEW action. */\n action?: string;\n /** Present on `addEntries` envelopes — the freshly-built journal\n * entries returned by the service. Each carries a server-stamped\n * `id` we use to highlight the row in JournalList. */\n entries?: { id?: string }[];\n /** Present on `voidEntry` envelopes — the kind=\"void-marker\" row\n * posted alongside the reversing entry. We surface this row (not\n * the reverseEntry) because the marker is the visual \"this entry\n * was voided here\" indicator the user is looking for. */\n markerEntry?: { id?: string };\n}\n\nconst props = defineProps<{ selectedResult?: ToolResultComplete<AccountingAppPayload, AccountingAppPayload> }>();\n\nconst TAB_KEYS = [\"journal\", \"opening\", \"accounts\", \"ledger\", \"balanceSheet\", \"profitLoss\", \"settings\"] as const;\ntype TabKey = (typeof TAB_KEYS)[number];\n\ninterface TabDef {\n key: TabKey;\n icon: string;\n labelKey: string;\n}\n\nconst TABS: readonly TabDef[] = [\n { key: \"journal\", icon: \"list\", labelKey: \"pluginAccounting.tabs.journal\" },\n { key: \"opening\", icon: \"play_arrow\", labelKey: \"pluginAccounting.tabs.opening\" },\n { key: \"accounts\", icon: \"list_alt\", labelKey: \"pluginAccounting.tabs.accounts\" },\n { key: \"ledger\", icon: \"menu_book\", labelKey: \"pluginAccounting.tabs.ledger\" },\n { key: \"balanceSheet\", icon: \"balance\", labelKey: \"pluginAccounting.tabs.balanceSheet\" },\n { key: \"profitLoss\", icon: \"trending_up\", labelKey: \"pluginAccounting.tabs.profitLoss\" },\n { key: \"settings\", icon: \"settings\", labelKey: \"pluginAccounting.tabs.settings\" },\n];\n\nfunction isTabKey(value: string | undefined): value is TabKey {\n return typeof value === \"string\" && (TAB_KEYS as readonly string[]).includes(value);\n}\n\nconst initialPayload = computed<AccountingAppPayload>(() => props.selectedResult?.data ?? props.selectedResult?.jsonData ?? {});\nconst initialTab = computed<TabKey>(() => (isTabKey(initialPayload.value.initialTab) ? initialPayload.value.initialTab : \"journal\"));\n\nconst currentTab = ref<TabKey>(initialTab.value);\nconst books = ref<BookSummary[]>([]);\nconst activeBookId = ref<string | null>(null);\nconst accounts = ref<Account[]>([]);\nconst loadingBooks = ref(true);\n// Sticky once the first books fetch lands. Lets the BookSwitcher stay\n// mounted across subsequent refetches (delete, create, pubsub-driven)\n// so the user sees the dropdown smoothly update its selection rather\n// than having the whole component flash in and out via `v-if`.\nconst initialLoadDone = ref(false);\n// First-run flow: when the user opens the app on a fresh\n// workspace (zero books), we render NewBookForm in full-page\n// mode in place of the regular chrome (header + tabs + main),\n// so the user MUST pick a name + currency before proceeding —\n// no popup, no dismiss. Distinct from the modal opened via\n// BookSwitcher's \"+ New book\" sentinel option, which reuses the\n// same component but with the overlay layout.\nconst showFirstRunForm = ref(false);\nconst firstRunHandled = ref(false);\n// Distinct from \"books is empty\" so we don't show the \"+ New\n// book\" CTA when the real problem is a transport / server failure\n// fetching the list.\nconst bookLoadError = ref<string | null>(null);\n// Tracks whether the active book has an opening entry on file.\n// `null` = unknown / loading; the gate only activates on an\n// explicit `false` so we don't disable tabs during the cold load\n// while the first getOpeningBalances request is still in flight.\nconst hasOpening = ref<boolean | null>(null);\n// Date of the active book's opening entry, plumbed down to the\n// DateRangePicker via the children so \"All\" can resolve to\n// (openingDate → today). `undefined` while loading / for books\n// without an opening on file (the opening gate prevents any tab\n// that would care from being shown in that state).\nconst activeOpeningDate = ref<string | undefined>(undefined);\n// Special \"you just deleted this book\" UI state. When set to a\n// non-null book name, the entire tab strip + main content are\n// replaced by an explicit \"<book> has been deleted — pick another\n// from the dropdown\" panel. Cleared the moment the user picks a\n// book from the BookSwitcher (or creates a new one). The View does\n// NOT auto-route to books[0] because that hides the fact that the\n// previously-active book is gone — issue #1126 (1) calls this\n// experience \"very confusing\".\nconst deletedNoticeName = ref<string | null>(null);\n\nconst activeBook = computed(() => books.value.find((book) => book.id === activeBookId.value) ?? null);\nconst activeBookName = computed(() => activeBook.value?.name ?? \"\");\nconst activeCurrency = computed(() => activeBook.value?.currency ?? \"USD\");\nconst activeCountry = computed(() => activeBook.value?.country);\nconst activeFiscalYearEnd = computed(() => activeBook.value?.fiscalYearEnd);\n\n// Single sync signal: every mutating service function publishes on\n// the accounting book channel after its write, so the sender's own\n// SSE round-trip drives the table/report refetch. No parallel\n// localVersion bump — it only ever fired the same watchers a second\n// time in the same tick.\nconst { version: bookVersion } = useAccountingChannel(activeBookId);\nuseAccountingBooksChannel(() => void refetchBooks());\n\nfunction pickInitialBookId(): string | null {\n // Priority: explicit `initialPayload.bookId` (carried in the\n // tool-result envelope by openBook / createBook / addEntries / …) →\n // first book in the list → null (empty workspace). The candidate\n // is validated against the live book list so a stale id from a\n // deleted book doesn't poison the View.\n if (books.value.length === 0) return null;\n const requested = initialPayload.value.bookId;\n if (requested && books.value.some((book) => book.id === requested)) return requested;\n return books.value[0].id;\n}\n\nasync function refetchBooks(): Promise<void> {\n loadingBooks.value = true;\n bookLoadError.value = null;\n // Capture the current active book BEFORE the fetch so we can\n // surface its name in the deleted-notice panel if the fetch\n // reveals it's gone. Without this snapshot, an SSE-driven refetch\n // racing ahead of the local deleteBook HTTP response would resolve\n // with `activeBook` already pointing at a now-stale entry.\n const previousActive = activeBook.value;\n try {\n const result = await getBooks();\n if (!result.ok) {\n // Surface load failures as a distinct error state so the user\n // doesn't see \"No books yet\" (and the auto-open modal) when\n // the real cause is a transport / server problem.\n bookLoadError.value = result.error;\n return;\n }\n books.value = result.data.books;\n // Sticky-true once a successful fetch lands. Setting it here (in\n // the success branch) rather than in `finally` means a first-load\n // transport / 5xx failure leaves BookSwitcher hidden — the user\n // sees only the `accounting-load-error` message rather than an\n // empty dropdown with a live \"+ New book\" path that has nothing\n // to fall back on.\n initialLoadDone.value = true;\n // While the deleted-notice panel is already up, leave activeBookId\n // alone — the user has to pick the next book themselves via\n // the BookSwitcher (and onBookSelected then clears the notice).\n // Otherwise pickInitialBookId would silently re-select books[0]\n // and undo the entire deletion-state UX.\n if (deletedNoticeName.value === null) {\n const stillExists = activeBookId.value !== null && books.value.some((book) => book.id === activeBookId.value);\n if (!stillExists) {\n // The active book just disappeared from the server's list.\n // Race-source possibilities, all converging here:\n // • local deleteBook → publishBooksChanged → SSE arrives\n // before the HTTP response handler can call onBookDeleted;\n // • a sibling tab / LLM tool deleted the book out-of-band.\n // In all cases the user needs to know what happened — show\n // the deleted-notice panel keyed off the previously-active\n // book's name, rather than silently snapping to books[0].\n // Falls back to the previous pickInitialBookId behaviour only\n // when there was no active book to lose (cold start).\n if (previousActive) {\n activeBookId.value = null;\n deletedNoticeName.value = previousActive.name;\n } else {\n activeBookId.value = pickInitialBookId();\n }\n }\n }\n // Auto-open the New Book modal exactly once on first arrival\n // when the workspace is empty. After that, the user can still\n // open it manually via the \"+ New book\" button.\n if (!firstRunHandled.value && books.value.length === 0) {\n firstRunHandled.value = true;\n showFirstRunForm.value = true;\n }\n } catch (err) {\n bookLoadError.value = errorMessage(err);\n } finally {\n loadingBooks.value = false;\n }\n}\n\nasync function onFirstBookCreated(book: BookSummary): Promise<void> {\n showFirstRunForm.value = false;\n await refetchBooks();\n activeBookId.value = book.id;\n}\n\n// Optimistically insert the new book and set the selection\n// BEFORE the refetch round-trip. Two reasons this beats the\n// previous await-refetch-then-select shape:\n// 1. The pubsub handler `useAccountingBooksChannel` fires its\n// own concurrent `refetchBooks` the instant the server\n// publishes books-changed. With await-then-select, that\n// concurrent refetch's stillExists guard reads the OLD\n// activeBookId (we haven't updated it yet) and — because\n// OLD is still in the books list — leaves the selection\n// pointing at OLD. Our update lands AFTER, but BookSwitcher\n// remounts under `v-if=\"!loadingBooks\"` mid-flight, so the\n// user sees the dropdown stick on OLD.\n// 2. With activeBookId already set to NEW and books pre-\n// populated to include NEW, every concurrent refetch's\n// stillExists check passes for NEW and leaves the selection\n// alone — order-independent by construction.\nasync function onBookCreated(book: BookSummary): Promise<void> {\n if (!books.value.some((existing) => existing.id === book.id)) {\n books.value = [...books.value, book];\n }\n activeBookId.value = book.id;\n // Creating a new book is also the \"exit\" out of the deleted-notice\n // panel — the user explicitly chose the new book, so re-enable the\n // tab strip and let the opening-gate watcher route them to Opening.\n deletedNoticeName.value = null;\n // currentTab may be on \"settings\" (the user opened the create\n // modal from there) — reset to journal so the openingGateActive\n // watcher's \"if (currentTab.value === 'opening') return\" gate\n // doesn't strand the user on settings while the gate is active.\n currentTab.value = \"journal\";\n await refetchBooks();\n}\n\nasync function refetchAccounts(): Promise<void> {\n if (!activeBookId.value) {\n accounts.value = [];\n return;\n }\n const result = await getAccounts(activeBookId.value);\n if (!result.ok) return;\n accounts.value = result.data.accounts;\n}\n\nasync function refetchOpening(): Promise<void> {\n if (!activeBookId.value) {\n hasOpening.value = null;\n activeOpeningDate.value = undefined;\n return;\n }\n const result = await getOpeningBalances(activeBookId.value);\n if (!result.ok) return;\n hasOpening.value = result.data.opening !== null;\n activeOpeningDate.value = result.data.opening?.date;\n}\n\n// A book without an opening on file is in \"gated\" mode: the user\n// must save an opening (empty is fine — see OpeningBalancesForm)\n// before journal / report tabs unlock. Settings stays accessible\n// so the user can delete the book if they don't want to proceed.\nconst openingGateActive = computed(() => activeBookId.value !== null && hasOpening.value === false);\n\n// Gated → only Opening + Settings render in the strip. Ungated →\n// Opening hides itself; users reach the form via the Edit button\n// on the active opening row in the journal, which transiently\n// switches `currentTab` to \"opening\" (kept visible while there).\nconst visibleTabs = computed<readonly TabDef[]>(() => {\n if (openingGateActive.value) return TABS.filter((tab) => tab.key === \"opening\" || tab.key === \"settings\");\n return TABS.filter((tab) => tab.key !== \"opening\" || currentTab.value === \"opening\");\n});\n\nfunction onBookSelected(bookId: string): void {\n activeBookId.value = bookId;\n // Picking a book from the dropdown is the explicit \"I'm done\n // looking at the deleted notice\" exit. Clear it so the tab strip\n // re-enables for the freshly selected book.\n deletedNoticeName.value = null;\n}\n\n// Entry id to surface in JournalList after an `addEntries` tool\n// result lands — the LLM just posted a journal entry and we want\n// the user's eye on the new row. Multi-entry batches highlight the\n// LAST entry only (matches the \"you ended up here\" intent of a\n// scroll-to-cursor).\nconst journalPreselectEntryId = ref<string | undefined>(undefined);\n\n// Account preselected by the Accounts tab → click handoff. Cleared\n// once the user picks a different account from the Ledger's own\n// dropdown so a stale preselection doesn't override later edits.\nconst ledgerPreselectAccountCode = ref<string | undefined>(undefined);\n\nfunction onAccountSelected(code: string): void {\n // Force the ref to a fresh value even when the user clicks the\n // same account a second time — the Ledger's `watch(preselect…)`\n // ignores no-op updates, so we'd otherwise leave the user on a\n // stale Ledger state if they navigated away and clicked back.\n ledgerPreselectAccountCode.value = undefined;\n Promise.resolve().then(() => {\n ledgerPreselectAccountCode.value = code;\n });\n currentTab.value = \"ledger\";\n}\n\nfunction onEntrySubmitted(): void {\n // After saving an opening, switch to the journal so the user\n // immediately sees the unlocked tabs. The server-side\n // publishBookChange triggers the bookVersion watcher over SSE,\n // which refetches hasOpening, so the gate auto-lifts shortly after\n // the tab switch — no manual unlock needed here. Normal entries\n // are now posted from the inline form inside JournalList; that\n // form drives its own dismissal and the journal repaints in\n // place.\n if (currentTab.value === \"opening\") {\n currentTab.value = \"journal\";\n }\n}\n\nasync function onBookDeleted(deletedName: string): Promise<void> {\n // Reset the tab BEFORE awaiting so a fast delete-then-create\n // can't race: if the new book's gate engages while we're still\n // awaiting refetchBooks, the gate watcher needs to see a\n // non-\"settings\" currentTab to route the user to Opening.\n currentTab.value = \"journal\";\n // Drop the active selection so refetchBooks doesn't auto-pick\n // books[0] — the user should see the deleted-notice panel and\n // explicitly switch via the BookSwitcher rather than be silently\n // moved to a different book (issue #1126).\n activeBookId.value = null;\n deletedNoticeName.value = deletedName;\n await refetchBooks();\n}\n\n// Refetch the chart of accounts whenever the active book changes\n// or any pub/sub / child action bumps bookVersion (e.g. an\n// upsertAccount from the Manage Accounts modal, or an LLM-driven\n// upsert in another tab). The list is small JSON; the cost of\n// over-fetching on entry / void / opening events is negligible\n// against the staleness bug it removes.\nwatch(\n () => [activeBookId.value, bookVersion.value],\n () => {\n if (activeBookId.value) void refetchAccounts();\n },\n { immediate: true },\n);\n\n// Drop any leftover Accounts → Ledger preselection when the active\n// book changes. Without this, picking account \"1000\" in book A's\n// Accounts tab and then switching to book B would carry the hint\n// across, so book B's Ledger would auto-select \"1000\" (which may\n// be an unrelated account in B's chart, or absent entirely).\nwatch(activeBookId, () => {\n ledgerPreselectAccountCode.value = undefined;\n});\n\n// Stash a target bookId that we want to land on but haven't been\n// able to apply yet (book not in `books` at the moment the\n// tool-result fired). Cleared as soon as the books list catches up.\nconst pendingTargetBookId = ref<string | null>(null);\n\nfunction applyTargetBookId(target: string): void {\n if (books.value.some((book) => book.id === target)) {\n activeBookId.value = target;\n pendingTargetBookId.value = null;\n return;\n }\n pendingTargetBookId.value = target;\n}\n\n// When the selected tool-result changes (user clicks a different\n// preview card in the sidebar), follow the new result's bookId so\n// the canvas lands on the book that action just touched. Skipped\n// when the new result has no bookId (silent reads / actions that\n// don't carry one). When the target isn't in `books` yet — common\n// race after a fresh `createBook → openBook(bookId)` handoff where\n// the result envelope arrives before refetchBooks completes — the\n// id is stashed and applied by the books watcher below as soon as\n// the list catches up.\nwatch(\n () => initialPayload.value.bookId,\n (next) => {\n if (!next) return;\n applyTargetBookId(next);\n },\n);\n\n// Drains the pending target once `books` includes it (typically\n// after a pub/sub-driven refetch resolves the createBook write).\n// No-op when nothing is pending or the target is still missing.\nwatch(books, () => {\n const pending = pendingTargetBookId.value;\n if (pending) applyTargetBookId(pending);\n});\n\n// Map a PREVIEW action to the canvas tab the user should land on.\n// Honours an explicit `initialTab` from the envelope (the LLM's\n// stated intent) over the action-default below — only `openBook`\n// currently ships initialTab, but the override is plugin-wide.\n//\n// The `balanceSheet` default for openBook / createBook /\n// setOpeningBalances assumes the book has an opening on file. For a\n// fresh book without one, the existing `openingGateActive` watcher\n// redirects to \"opening\" — we don't try to short-circuit that here\n// because hasOpening hasn't necessarily resolved when this runs.\nfunction pickTabForAction(payload: AccountingAppPayload): TabKey | null {\n if (isTabKey(payload.initialTab)) return payload.initialTab;\n switch (payload.action) {\n case ACCOUNTING_ACTIONS.addEntries:\n case ACCOUNTING_ACTIONS.voidEntry:\n return \"journal\";\n case ACCOUNTING_ACTIONS.upsertAccount:\n return \"accounts\";\n case ACCOUNTING_ACTIONS.updateBook:\n return \"settings\";\n case ACCOUNTING_ACTIONS.openBook:\n case ACCOUNTING_ACTIONS.createBook:\n case ACCOUNTING_ACTIONS.setOpeningBalances:\n return \"balanceSheet\";\n default:\n return null;\n }\n}\n\n// For tool results that should auto-expand a row in JournalList,\n// derive the entry id from the action's payload. addEntries picks\n// the LAST entry in the batch (\"you ended up here\" cursor); voidEntry\n// picks the void-MARKER (the visual \"voided here\" indicator), not\n// the reversing entry.\nfunction pickJournalPreselectId(payload: AccountingAppPayload): string | undefined {\n if (payload.action === ACCOUNTING_ACTIONS.addEntries) {\n const entries = Array.isArray(payload.entries) ? payload.entries : [];\n return entries[entries.length - 1]?.id;\n }\n if (payload.action === ACCOUNTING_ACTIONS.voidEntry) {\n return payload.markerEntry?.id;\n }\n return undefined;\n}\n\n// Drive canvas tab + journal preselect from the active tool-result\n// envelope. The route handler stamps `data: { action, bookId, … }`\n// onto every PREVIEW action's response (server/api/routes/\n// accounting.ts dispatch + PREVIEW_ACTIONS). `immediate: true` so a\n// cold open with the result already selected (e.g., reload after\n// the LLM dispatched) routes to the right surface too.\n//\n// Preselect is *always* assigned (not `if (preselect)`) so a\n// subsequent non-addEntries/voidEntry tool result clears any stale\n// id left over from a prior addEntries the user has already seen —\n// otherwise the next JournalList remount would replay it. The child\n// also emits `preselectConsumed` after expanding for the same\n// reason.\nwatch(\n () => initialPayload.value,\n (payload) => {\n const targetTab = pickTabForAction(payload);\n if (targetTab) currentTab.value = targetTab;\n journalPreselectEntryId.value = pickJournalPreselectId(payload);\n },\n { immediate: true },\n);\n\n// Drop the journal preselect on a real book SWITCH — leftover ids\n// from the prior book don't exist in the new one. The cold-load\n// transition (null → bookId) doesn't qualify: refetchBooks resolves\n// activeBookId asynchronously and would otherwise clobber a\n// preselect the addEntries watcher just set on initial mount.\nwatch(activeBookId, (_next, prev) => {\n if (!prev) return;\n journalPreselectEntryId.value = undefined;\n});\n\n// Refetch the opening status whenever the active book changes or\n// any pub/sub / child action bumps bookVersion (e.g. an opening\n// got saved or voided). Clears hasOpening when the book goes null\n// so a stale \"true\" doesn't carry over between books.\nwatch(\n () => [activeBookId.value, bookVersion.value],\n () => void refetchOpening(),\n { immediate: true },\n);\n\n// Force-route to the Opening tab whenever the gate engages.\n// Other tabs are hidden from the strip while gated, but this\n// watcher handles the programmatic case where currentTab still\n// points at a now-hidden tab (book switch, initial mount with a\n// no-opening book, LLM-supplied initialTab pointing at a gated\n// tab, or fresh-book creation right after deleting from the\n// settings tab) — without it, `<main>` would render nothing or\n// the user would be stranded on the prior book's settings view.\n// We don't exempt \"settings\" here: the user can still click back\n// to it from the (gated) tab strip if they want to delete the\n// new book instead of setting it up.\nwatch(openingGateActive, (active) => {\n if (!active) return;\n if (currentTab.value === \"opening\") return;\n currentTab.value = \"opening\";\n});\n\nvoid refetchBooks();\n</script>\n","<template>\n <!-- Compact inline summary for non-openBook tool results. The\n openBook envelope routes to View.vue (full app) instead of\n this component; everything that lands here is a\n compact-result action (addEntries, getReport, …). -->\n <div class=\"text-sm text-gray-700\" data-testid=\"accounting-preview\">\n <span class=\"material-icons text-base align-middle mr-1\">account_balance</span>\n <span>{{ summary }}</span>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { formatAmountNumeric } from \"../shared\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ data?: unknown; jsonData?: Record<string, unknown> }>();\n\ninterface BalanceSheetSection {\n type: string;\n total?: number;\n}\ninterface BalanceSheetLike {\n balanceSheet?: { asOf?: string; sections?: BalanceSheetSection[]; imbalance?: number };\n}\ninterface ProfitLossLike {\n profitLoss?: { from?: string; to?: string; netIncome?: number };\n}\ninterface EntriesLike {\n entries?: { id?: string; date?: string }[];\n}\ninterface BookLike {\n book?: { id?: string; name?: string };\n}\n\n// Each summarise* helper returns null when its branch doesn't apply,\n// keeping the dispatch in `summary` linear (no nested if-trees).\n\nfunction summariseError(json: Record<string, unknown>): string | null {\n const { error } = json as { error?: unknown };\n if (typeof error !== \"string\") return null;\n return t(\"pluginAccounting.previewError\", { error });\n}\n\nfunction summariseEntry(json: Record<string, unknown>): string | null {\n // addEntries returns `{ entries: [...] }`. The compact preview\n // card shows one date — use the first entry's so single-entry\n // batches (the common case from the manual UI) read naturally\n // and multi-entry batches still anchor to a meaningful date.\n const { entries } = json as EntriesLike;\n if (!Array.isArray(entries) || entries.length === 0) return null;\n const [first] = entries;\n if (!first?.id || !first?.date) return null;\n return t(\"pluginAccounting.preview.entry\", { date: first.date });\n}\n\nfunction summarisePl(json: Record<string, unknown>): string | null {\n const { profitLoss } = json as ProfitLossLike;\n if (!profitLoss || typeof profitLoss.netIncome !== \"number\") return null;\n return t(\"pluginAccounting.preview.pl\", {\n from: profitLoss.from ?? \"?\",\n to: profitLoss.to ?? \"?\",\n net: formatAmountNumeric(profitLoss.netIncome),\n });\n}\n\nfunction summariseBs(json: Record<string, unknown>): string | null {\n const { balanceSheet } = json as BalanceSheetLike;\n if (!balanceSheet?.asOf || !balanceSheet.sections) return null;\n const assets = balanceSheet.sections.find((section) => section.type === \"asset\");\n return t(\"pluginAccounting.preview.bs\", {\n date: balanceSheet.asOf,\n assets: assets ? formatAmountNumeric(assets.total ?? 0) : \"?\",\n });\n}\n\nfunction summariseBook(json: Record<string, unknown>): string | null {\n const { book } = json as BookLike;\n if (!book?.id || !book?.name) return null;\n return t(\"pluginAccounting.preview.bookCreated\", { name: book.name, id: book.id });\n}\n\nfunction summariseFallback(json: Record<string, unknown>): string {\n const { bookId } = json as { bookId?: unknown };\n if (typeof bookId === \"string\") return t(\"pluginAccounting.previewSummary\", { bookId });\n return t(\"pluginAccounting.previewGeneric\");\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n // Some renderers pass the structured payload via `data`, others\n // via `jsonData`. Accept either so a tool-result like\n // `{ entry: ... }` resolves to the right summariser regardless\n // of which prop the host harness picks.\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : {};\n}\n\nconst summary = computed<string>(() => {\n const json = { ...asObject(props.data), ...asObject(props.jsonData) };\n return summariseError(json) ?? summariseEntry(json) ?? summarisePl(json) ?? summariseBs(json) ?? summariseBook(json) ?? summariseFallback(json);\n});\n</script>\n","<template>\n <!-- Compact inline summary for non-openBook tool results. The\n openBook envelope routes to View.vue (full app) instead of\n this component; everything that lands here is a\n compact-result action (addEntries, getReport, …). -->\n <div class=\"text-sm text-gray-700\" data-testid=\"accounting-preview\">\n <span class=\"material-icons text-base align-middle mr-1\">account_balance</span>\n <span>{{ summary }}</span>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { formatAmountNumeric } from \"../shared\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{ data?: unknown; jsonData?: Record<string, unknown> }>();\n\ninterface BalanceSheetSection {\n type: string;\n total?: number;\n}\ninterface BalanceSheetLike {\n balanceSheet?: { asOf?: string; sections?: BalanceSheetSection[]; imbalance?: number };\n}\ninterface ProfitLossLike {\n profitLoss?: { from?: string; to?: string; netIncome?: number };\n}\ninterface EntriesLike {\n entries?: { id?: string; date?: string }[];\n}\ninterface BookLike {\n book?: { id?: string; name?: string };\n}\n\n// Each summarise* helper returns null when its branch doesn't apply,\n// keeping the dispatch in `summary` linear (no nested if-trees).\n\nfunction summariseError(json: Record<string, unknown>): string | null {\n const { error } = json as { error?: unknown };\n if (typeof error !== \"string\") return null;\n return t(\"pluginAccounting.previewError\", { error });\n}\n\nfunction summariseEntry(json: Record<string, unknown>): string | null {\n // addEntries returns `{ entries: [...] }`. The compact preview\n // card shows one date — use the first entry's so single-entry\n // batches (the common case from the manual UI) read naturally\n // and multi-entry batches still anchor to a meaningful date.\n const { entries } = json as EntriesLike;\n if (!Array.isArray(entries) || entries.length === 0) return null;\n const [first] = entries;\n if (!first?.id || !first?.date) return null;\n return t(\"pluginAccounting.preview.entry\", { date: first.date });\n}\n\nfunction summarisePl(json: Record<string, unknown>): string | null {\n const { profitLoss } = json as ProfitLossLike;\n if (!profitLoss || typeof profitLoss.netIncome !== \"number\") return null;\n return t(\"pluginAccounting.preview.pl\", {\n from: profitLoss.from ?? \"?\",\n to: profitLoss.to ?? \"?\",\n net: formatAmountNumeric(profitLoss.netIncome),\n });\n}\n\nfunction summariseBs(json: Record<string, unknown>): string | null {\n const { balanceSheet } = json as BalanceSheetLike;\n if (!balanceSheet?.asOf || !balanceSheet.sections) return null;\n const assets = balanceSheet.sections.find((section) => section.type === \"asset\");\n return t(\"pluginAccounting.preview.bs\", {\n date: balanceSheet.asOf,\n assets: assets ? formatAmountNumeric(assets.total ?? 0) : \"?\",\n });\n}\n\nfunction summariseBook(json: Record<string, unknown>): string | null {\n const { book } = json as BookLike;\n if (!book?.id || !book?.name) return null;\n return t(\"pluginAccounting.preview.bookCreated\", { name: book.name, id: book.id });\n}\n\nfunction summariseFallback(json: Record<string, unknown>): string {\n const { bookId } = json as { bookId?: unknown };\n if (typeof bookId === \"string\") return t(\"pluginAccounting.previewSummary\", { bookId });\n return t(\"pluginAccounting.previewGeneric\");\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n // Some renderers pass the structured payload via `data`, others\n // via `jsonData`. Accept either so a tool-result like\n // `{ entry: ... }` resolves to the right summariser regardless\n // of which prop the host harness picks.\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : {};\n}\n\nconst summary = computed<string>(() => {\n const json = { ...asObject(props.data), ...asObject(props.jsonData) };\n return summariseError(json) ?? summariseEntry(json) ?? summarisePl(json) ?? summariseBs(json) ?? summariseBook(json) ?? summariseFallback(json);\n});\n</script>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vue.js","names":[],"sources":["../src/vue/hostContext.ts","../src/vue/lang/en.ts","../src/vue/lang/ja.ts","../src/vue/lang/zh.ts","../src/vue/lang/ko.ts","../src/vue/lang/es.ts","../src/vue/lang/ptBR.ts","../src/vue/lang/fr.ts","../src/vue/lang/de.ts","../src/vue/lang/index.ts","../src/vue/api.ts","../src/vue/components/NewBookForm.vue","../src/vue/components/NewBookForm.vue","../src/vue/components/BookSwitcher.vue","../src/vue/components/BookSwitcher.vue","../src/vue/components/useLatestRequest.ts","../src/vue/components/DateRangePicker.vue","../src/vue/components/DateRangePicker.vue","../src/vue/components/accountNumbering.ts","../src/vue/components/AccountRow.vue","../src/vue/components/AccountRow.vue","../src/vue/components/accountValidation.ts","../src/vue/components/AccountEditor.vue","../src/vue/components/AccountEditor.vue","../src/vue/components/AccountsModal.vue","../src/vue/components/AccountsModal.vue","../src/vue/components/JournalEntryForm.vue","../src/vue/components/JournalEntryForm.vue","../src/vue/components/JournalList.vue","../src/vue/components/JournalList.vue","../src/vue/components/OpeningBalancesForm.vue","../src/vue/components/OpeningBalancesForm.vue","../src/vue/components/AccountsList.vue","../src/vue/components/AccountsList.vue","../src/vue/components/Ledger.vue","../src/vue/components/Ledger.vue","../src/vue/components/BalanceSheet.vue","../src/vue/components/BalanceSheet.vue","../src/vue/components/ProfitLoss.vue","../src/vue/components/ProfitLoss.vue","../src/vue/components/BookSettings.vue","../src/vue/components/BookSettings.vue","../src/vue/useAccountingChannel.ts","../src/vue/View.vue","../src/vue/View.vue","../src/vue/Preview.vue","../src/vue/Preview.vue"],"sourcesContent":["// Host-injected runtime context for the accounting Vue surface.\n//\n// The package can't reach into the host for its network client or its\n// raw pub/sub transport (that would be an uphill import, and would\n// hard-wire the package to MulmoClaude's internals). Instead the host\n// injects them once at startup via `configureAccountingHost(...)`, the\n// same module-level DI pattern `@mulmoclaude/collection-plugin` uses\n// (`configureCollectionUi`). MulmoTerminal wires its own equivalents.\n//\n// Two seams:\n// · apiCall — POST to /api/accounting; the host attaches the bearer\n// token + base URL. Returns the shared `ApiResult` union.\n// · subscribe — raw pub/sub channel subscription (socket.io in the\n// MulmoClaude host). The accounting backend publishes on\n// raw `accounting:<bookId>` channels, so the View needs\n// the raw transport, not the plugin-scoped pub/sub.\n\n/** Mirrors the host's `ApiResult<T>` (src/utils/api.ts) so callers\n * pattern-match on `.ok` without depending on the host module. */\nexport type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string; status: number };\n\n/** Network seam — structurally compatible with the host's `apiCall`.\n * `method` mirrors the host `ApiOptions[\"method\"]` union so the host\n * can pass its `apiCall` straight in without an adapter. */\nexport type AccountingApiCall = <T = unknown>(\n path: string,\n opts: { method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\"; body?: unknown },\n) => Promise<ApiResult<T>>;\n\n/** Pub/sub seam — structurally compatible with `usePubSub().subscribe`. */\nexport type AccountingSubscribe = (channel: string, handler: (payload: unknown) => void) => () => void;\n\n/** Locale seam — the host's active i18n locale tag (e.g. \"en\", \"ja\"), read\n * reactively. The plugin owns a self-contained vue-i18n instance and mirrors\n * this tag onto it, so it shares NO i18n resources with the host. */\nexport type AccountingLocaleTag = () => string;\n\nexport interface AccountingHostContext {\n apiCall: AccountingApiCall;\n subscribe: AccountingSubscribe;\n localeTag: AccountingLocaleTag;\n}\n\nlet ctx: AccountingHostContext | null = null;\n\n/** Called once by the host before any accounting View mounts. */\nexport function configureAccountingHost(context: AccountingHostContext): void {\n ctx = context;\n}\n\nfunction requireCtx(): AccountingHostContext {\n if (!ctx) {\n throw new Error(\"@mulmoclaude/accounting-plugin: configureAccountingHost() must be called before the accounting View mounts\");\n }\n return ctx;\n}\n\nexport function hostApiCall<T = unknown>(path: string, opts: { method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\"; body?: unknown }): Promise<ApiResult<T>> {\n return requireCtx().apiCall<T>(path, opts);\n}\n\nexport function hostSubscribe(channel: string, handler: (payload: unknown) => void): () => void {\n return requireCtx().subscribe(channel, handler);\n}\n\n/** The host's active i18n locale tag, read reactively by the plugin's own\n * vue-i18n instance (see `./lang`). */\nexport function hostLocaleTag(): string {\n return requireCtx().localeTag();\n}\n","// Auto-extracted from the host src/lang/en.ts during the accounting-plugin\n// i18n migration. The plugin owns its own copy so it uses NO host i18n resources.\nconst enMessages = {\n pluginAccounting: {\n title: \"Accounting\",\n noBook: 'No books yet — click \"+ New book\" above to create your first one.',\n common: {\n cancel: \"Cancel\",\n loading: \"Loading…\",\n error: \"Error: {error}\",\n empty: \"No entries yet.\",\n },\n tabs: {\n journal: \"Journal\",\n newEntry: \"New entry\",\n opening: \"Opening\",\n accounts: \"Accounts\",\n ledger: \"Ledger\",\n balanceSheet: \"Balance sheet\",\n profitLoss: \"P&L\",\n settings: \"Settings\",\n },\n bookSwitcher: {\n label: \"Book\",\n newBook: \"New book…\",\n create: \"Create\",\n nameLabel: \"Name\",\n currencyLabel: \"Currency\",\n countryLabel: \"Country\",\n countryPlaceholder: \"Select a country…\",\n countryHint: \"The country sets the tax jurisdiction so the assistant can give country-specific advice (T-number for Japan, VAT ID for the EU, etc.).\",\n fiscalYearEndLabel: \"Fiscal year end\",\n fiscalYearEndQ1: \"March 31 (Q1)\",\n fiscalYearEndQ2: \"June 30 (Q2)\",\n fiscalYearEndQ3: \"September 30 (Q3)\",\n fiscalYearEndQ4: \"December 31 (Q4)\",\n fiscalYearEndHint: \"Determines the fiscal year boundary used by the date-range shortcuts in this book. Default is December 31 (Q4 — calendar year).\",\n placeholder: \"Pick a book…\",\n firstRunHint:\n \"Pick a name, currency, country, and fiscal year end for your first book. The currency is set per-book and is hard to change once you start booking entries.\",\n },\n deletedNotice: {\n title: '\"{bookName}\" has been deleted.',\n body: \"Pick another book from the dropdown above, or create a new one.\",\n },\n journalList: {\n fromLabel: \"From\",\n toLabel: \"To\",\n accountLabel: \"Account\",\n allAccounts: \"All accounts\",\n void: \"Void\",\n edit: \"Edit\",\n voidConfirm: \"Void this entry? The original stays in the journal; a reversing pair is appended.\",\n voidReason: \"Reason (optional):\",\n columns: { date: \"Date\", kind: \"Kind\", memo: \"Memo\", lines: \"Lines\" },\n kind: { normal: \"—\", opening: \"Opening\", void: \"Reverse\", voidMarker: \"Void marker\" },\n },\n entryForm: {\n title: \"New journal entry\",\n editTitle: \"Edit journal entry\",\n editBanner: \"When you submit, the original entry will be voided and replaced by this one.\",\n dateLabel: \"Date\",\n memoLabel: \"Memo\",\n accountLabel: \"Account\",\n debitLabel: \"Debit\",\n creditLabel: \"Credit\",\n taxRegistrationIdLabel: \"Tax registration ID\",\n taxRegistrationIdPlaceholder: \"T-number / VAT ID / GSTIN…\",\n taxRegistrationIdMissingWarning: \"Required\",\n addLine: \"Add line\",\n removeLine: \"Remove\",\n submit: \"Post entry\",\n submitting: \"Posting…\",\n update: \"Update entry\",\n updating: \"Updating…\",\n cancelEdit: \"Cancel\",\n success: \"Entry posted.\",\n editSuccess: \"Entry updated.\",\n editVoidReason: \"edited\",\n imbalance: \"Imbalance: {amount}\",\n balanced: \"Balanced ✓\",\n },\n openingForm: {\n title: \"Opening balances\",\n asOfLabel: \"As of\",\n explainer:\n \"Enter your balance-sheet positions as of the start date. Income / expense accounts are not allowed here. Σ debit must equal Σ credit; the difference plug-in is Retained Earnings.\",\n emptyHint:\n \"It's fine to save with everything blank — you can update opening balances later. The book just needs an opening on file before other tabs unlock.\",\n explainer2: \"Balance-sheet accounts only.\",\n submit: \"Save opening balances\",\n replaceWarning: \"Saving replaces the existing opening (the old one is voided in the journal).\",\n none: \"No opening balance set yet.\",\n setBy: \"Set as of {date}\",\n success: \"Opening balances saved.\",\n },\n ledger: {\n selectAccount: \"Select account\",\n closingBalance: \"Closing balance\",\n columns: { date: \"Date\", memo: \"Memo\", debit: \"Debit\", credit: \"Credit\", balance: \"Balance\", taxRegistrationId: \"Tax registration ID\" },\n },\n dateRange: {\n shortcutLabel: \"Range\",\n currentQuarter: \"Current quarter\",\n previousQuarter: \"Last quarter\",\n currentYear: \"Current year\",\n previousYear: \"Last year\",\n lifetime: \"Lifetime\",\n all: \"All\",\n fromLabel: \"From\",\n toLabel: \"To\",\n },\n balanceSheet: {\n asOfLabel: \"Period\",\n sections: { asset: \"Assets\", liability: \"Liabilities\", equity: \"Equity\" },\n total: \"Total\",\n imbalance: \"Imbalance: {amount}\",\n currentEarnings: \"Current period earnings\",\n shortcutLabel: \"Shortcut\",\n thisMonth: \"This month\",\n lastMonth: \"Last month\",\n lastQuarter: \"Last quarter\",\n lastYear: \"Last year\",\n },\n profitLoss: { fromLabel: \"From\", toLabel: \"To\", income: \"Income\", expense: \"Expense\", netIncome: \"Net income:\" },\n accounts: {\n listEmpty: \"No accounts in this category yet.\",\n openLedgerAria: \"Open ledger for {code} {name}\",\n manageButton: \"Manage accounts\",\n modalTitle: \"Manage accounts\",\n addAccount: \"Add account\",\n sectionTitle: {\n asset: \"Assets\",\n liability: \"Liabilities\",\n equity: \"Equity\",\n income: \"Income\",\n expense: \"Expenses\",\n },\n columnCode: \"Code\",\n columnName: \"Name\",\n columnType: \"Type\",\n columnNote: \"Note\",\n typeOption: {\n asset: \"Asset\",\n liability: \"Liability\",\n equity: \"Equity\",\n income: \"Income\",\n expense: \"Expense\",\n },\n edit: \"Edit\",\n save: \"Save\",\n cancel: \"Cancel\",\n saving: \"Saving…\",\n addToCategory: \"Add {type} account\",\n deactivate: \"Deactivate\",\n reactivate: \"Activate\",\n deactivateConfirm: 'Hide \"{name}\" from entry/ledger dropdowns? Existing journal entries are unaffected.',\n errorEmptyCode: \"Code is required.\",\n errorReservedCode: \"Codes starting with “_” are reserved for system rows.\",\n errorInvalidCodeFormat: \"Account code must be exactly 4 digits.\",\n errorCodeTypeMismatch: \"Code must match the account type: 1xxx asset, 2xxx liability, 3xxx equity, 4xxx income, 5xxx expense.\",\n errorEmptyName: \"Name is required.\",\n errorDuplicateCode: \"An account with this code already exists.\",\n errorDuplicateName: \"An account with this name already exists.\",\n success: \"Account saved.\",\n codeReadOnlyHint: \"Code can't be changed once an account is created.\",\n noteOptional: \"(optional)\",\n },\n settings: {\n bookInfo: \"Book information\",\n bookInfoExplain: \"Edit the country to update tax-jurisdiction-specific advice. Currency cannot be changed once entries are booked.\",\n countryUnset: \"(not set)\",\n fiscalYearEndExplain: \"Changes only how date-range shortcuts resolve from now on; existing journal entries are not moved.\",\n saveChanges: \"Save changes\",\n updateOk: \"Book updated.\",\n rebuild: \"Rebuild snapshots\",\n rebuildExplain: \"Drops every cached monthly snapshot and recomputes them from the journal. Safe to run; useful after editing journal files by hand.\",\n rebuildOk: \"Rebuilt {count} period(s).\",\n advanced: \"Advanced…\",\n deleteBook: \"Delete book\",\n deleteBookExplain: \"Permanently deletes this book's directory. Cannot be undone.\",\n deleteBookConfirm: 'Type \"{bookName}\" to confirm:',\n deleteBookButton: \"Delete forever\",\n },\n preview: {\n entry: \"Posted entry on {date}\",\n pl: \"P&L {from} → {to}: net {net}\",\n bs: \"Balance sheet as of {date} · assets {assets}\",\n bookCreated: 'Created book \"{name}\" ({id})',\n },\n previewSummary: \"Accounting · {bookId}\",\n previewError: \"Accounting: {error}\",\n previewGeneric: \"Accounting result\",\n },\n};\n\nexport default enMessages;\n\nexport type AccountingMessages = typeof enMessages;\n","// Auto-extracted from the host src/lang/ja.ts during the accounting-plugin\n// i18n migration. The plugin owns its own copy so it uses NO host i18n resources.\nimport type { AccountingMessages } from \"./en\";\n\nconst jaMessages: AccountingMessages = {\n pluginAccounting: {\n title: \"会計\",\n noBook: 'まだ帳簿がありません — 上の \"+ 新しい帳簿\" から作成してください。',\n common: {\n cancel: \"キャンセル\",\n loading: \"読み込み中…\",\n error: \"エラー: {error}\",\n empty: \"まだエントリがありません。\",\n },\n tabs: {\n journal: \"仕訳\",\n newEntry: \"新規仕訳\",\n opening: \"期首残高\",\n accounts: \"勘定科目\",\n ledger: \"元帳\",\n balanceSheet: \"貸借対照表\",\n profitLoss: \"損益\",\n settings: \"設定\",\n },\n bookSwitcher: {\n label: \"帳簿\",\n newBook: \"新しい帳簿…\",\n create: \"作成\",\n nameLabel: \"名前\",\n currencyLabel: \"通貨\",\n countryLabel: \"国\",\n countryPlaceholder: \"国を選択…\",\n countryHint: \"国を設定すると税の管轄が決まり、アシスタントが国別のアドバイス(日本のT番号、EUのVAT IDなど)を提供できます。\",\n fiscalYearEndLabel: \"会計年度末日\",\n fiscalYearEndQ1: \"3月31日 (Q1)\",\n fiscalYearEndQ2: \"6月30日 (Q2)\",\n fiscalYearEndQ3: \"9月30日 (Q3)\",\n fiscalYearEndQ4: \"12月31日 (Q4)\",\n fiscalYearEndHint: \"この帳簿の日付範囲ショートカットで使う会計年度の境界を決めます。デフォルトは12月31日(Q4 — 暦年)。\",\n placeholder: \"帳簿を選択…\",\n firstRunHint: \"最初の帳簿の名前・通貨・国・会計年度末日を選んでください。通貨は帳簿ごとに固定で、仕訳を入力した後の変更は難しくなります。\",\n },\n deletedNotice: {\n title: \"「{bookName}」を削除しました。\",\n body: \"上のドロップダウンから別の帳簿を選ぶか、新しい帳簿を作成してください。\",\n },\n journalList: {\n fromLabel: \"開始日\",\n toLabel: \"終了日\",\n accountLabel: \"勘定科目\",\n allAccounts: \"すべての科目\",\n void: \"取消\",\n edit: \"編集\",\n voidConfirm: \"この仕訳を取り消しますか?元の仕訳は journal に残り、逆仕訳が追記されます。\",\n voidReason: \"理由(任意):\",\n columns: {\n date: \"日付\",\n kind: \"種別\",\n memo: \"メモ\",\n lines: \"行\",\n },\n kind: {\n normal: \"—\",\n opening: \"期首\",\n void: \"逆仕訳\",\n voidMarker: \"取消マーカー\",\n },\n },\n entryForm: {\n title: \"新規仕訳\",\n editTitle: \"仕訳の編集\",\n editBanner: \"送信すると、元の仕訳は取り消され、この内容で置き換えられます。\",\n dateLabel: \"日付\",\n memoLabel: \"メモ\",\n accountLabel: \"勘定科目\",\n debitLabel: \"借方\",\n creditLabel: \"貸方\",\n taxRegistrationIdLabel: \"登録番号\",\n taxRegistrationIdPlaceholder: \"T1234567890123\",\n taxRegistrationIdMissingWarning: \"必須\",\n addLine: \"行を追加\",\n removeLine: \"削除\",\n submit: \"起票\",\n submitting: \"起票中…\",\n update: \"更新\",\n updating: \"更新中…\",\n cancelEdit: \"キャンセル\",\n success: \"起票しました。\",\n editSuccess: \"仕訳を更新しました。\",\n editVoidReason: \"編集\",\n imbalance: \"差額: {amount}\",\n balanced: \"バランス OK ✓\",\n },\n openingForm: {\n title: \"期首残高\",\n asOfLabel: \"基準日\",\n explainer: \"B/S 科目(資産・負債・純資産)のみ入力可能。Σ借方=Σ貸方を満たす必要があり、差額は Retained Earnings に集約されます。\",\n emptyHint:\n \"すべて空欄のまま保存しても構いません — 期首残高は後から更新できます。他のタブを利用するには、いったん期首残高が登録されている必要があります。\",\n explainer2: \"B/S 科目のみ。\",\n submit: \"期首残高を保存\",\n replaceWarning: \"保存すると既存の期首残高は journal で取り消されます。\",\n none: \"期首残高は未設定です。\",\n setBy: \"{date} 時点で設定済み\",\n success: \"期首残高を保存しました。\",\n },\n ledger: {\n selectAccount: \"科目を選択\",\n closingBalance: \"期末残高\",\n columns: {\n date: \"日付\",\n memo: \"メモ\",\n debit: \"借方\",\n credit: \"貸方\",\n balance: \"残高\",\n taxRegistrationId: \"登録番号\",\n },\n },\n dateRange: {\n shortcutLabel: \"範囲\",\n currentQuarter: \"今四半期\",\n previousQuarter: \"前四半期\",\n currentYear: \"今年度\",\n previousYear: \"前年度\",\n lifetime: \"開設以来\",\n all: \"すべて\",\n fromLabel: \"開始日\",\n toLabel: \"終了日\",\n },\n balanceSheet: {\n asOfLabel: \"対象月\",\n sections: {\n asset: \"資産\",\n liability: \"負債\",\n equity: \"純資産\",\n },\n total: \"合計\",\n imbalance: \"差額: {amount}\",\n currentEarnings: \"当期純損益\",\n shortcutLabel: \"ショートカット\",\n thisMonth: \"今月\",\n lastMonth: \"先月\",\n lastQuarter: \"前四半期\",\n lastYear: \"前年\",\n },\n profitLoss: {\n fromLabel: \"開始日\",\n toLabel: \"終了日\",\n income: \"収益\",\n expense: \"費用\",\n netIncome: \"当期純利益:\",\n },\n accounts: {\n listEmpty: \"このカテゴリにはまだ勘定科目がありません。\",\n openLedgerAria: \"{code} {name} の元帳を開く\",\n manageButton: \"勘定科目を管理\",\n modalTitle: \"勘定科目の管理\",\n addAccount: \"勘定科目を追加\",\n sectionTitle: {\n asset: \"資産\",\n liability: \"負債\",\n equity: \"純資産\",\n income: \"収益\",\n expense: \"費用\",\n },\n columnCode: \"コード\",\n columnName: \"名称\",\n columnType: \"区分\",\n columnNote: \"メモ\",\n typeOption: {\n asset: \"資産\",\n liability: \"負債\",\n equity: \"純資産\",\n income: \"収益\",\n expense: \"費用\",\n },\n edit: \"編集\",\n save: \"保存\",\n cancel: \"キャンセル\",\n saving: \"保存中…\",\n addToCategory: \"{type}を追加\",\n deactivate: \"無効化\",\n reactivate: \"有効化\",\n deactivateConfirm: \"「{name}」を仕訳・元帳の選択肢から非表示にしますか?既存の仕訳には影響しません。\",\n errorEmptyCode: \"コードは必須です。\",\n errorReservedCode: \"「_」で始まるコードはシステム用に予約されています。\",\n errorInvalidCodeFormat: \"勘定科目コードは4桁の数字で入力してください。\",\n errorCodeTypeMismatch: \"コードの先頭桁は勘定科目タイプと一致させてください: 1xxx 資産, 2xxx 負債, 3xxx 純資産, 4xxx 収益, 5xxx 費用。\",\n errorEmptyName: \"名称は必須です。\",\n errorDuplicateCode: \"このコードの勘定科目は既に存在します。\",\n errorDuplicateName: \"この名称の勘定科目は既に存在します。\",\n success: \"勘定科目を保存しました。\",\n codeReadOnlyHint: \"コードは作成後に変更できません。\",\n noteOptional: \"(任意)\",\n },\n settings: {\n bookInfo: \"帳簿情報\",\n bookInfoExplain: \"国を編集すると税の管轄に応じたアドバイスが更新されます。通貨は仕訳を入力した後は変更できません。\",\n countryUnset: \"(未設定)\",\n fiscalYearEndExplain: \"今後の日付範囲ショートカットの解決方法のみが変わります。既存の仕訳は移動しません。\",\n saveChanges: \"変更を保存\",\n updateOk: \"帳簿を更新しました。\",\n rebuild: \"スナップショット再構築\",\n rebuildExplain: \"月次スナップショットキャッシュをすべて削除し、journal から再計算します。journal を手で編集した後のリカバリに使えます。\",\n rebuildOk: \"{count} 期間を再構築しました。\",\n advanced: \"詳細設定…\",\n deleteBook: \"帳簿を削除\",\n deleteBookExplain: \"この帳簿のディレクトリを完全に削除します。元に戻せません。\",\n deleteBookConfirm: '確認のため \"{bookName}\" を入力してください:',\n deleteBookButton: \"完全に削除\",\n },\n preview: {\n entry: \"{date} の仕訳を起票\",\n pl: \"P&L {from} → {to}: 純損益 {net}\",\n bs: \"{date} 時点の B/S · 資産 {assets}\",\n bookCreated: '帳簿 \"{name}\" ({id}) を作成',\n },\n previewSummary: \"会計 · {bookId}\",\n previewError: \"会計: {error}\",\n previewGeneric: \"会計の結果\",\n },\n};\n\nexport default jaMessages;\n","// Auto-extracted from the host src/lang/zh.ts during the accounting-plugin\n// i18n migration. The plugin owns its own copy so it uses NO host i18n resources.\nimport type { AccountingMessages } from \"./en\";\n\nconst zhMessages: AccountingMessages = {\n pluginAccounting: {\n title: \"会计\",\n noBook: '尚无账簿 — 点击上方 \"+ 新建账簿\" 创建第一个。',\n common: {\n cancel: \"取消\",\n loading: \"加载中…\",\n error: \"错误: {error}\",\n empty: \"尚无条目。\",\n },\n tabs: {\n journal: \"日记账\",\n newEntry: \"新建分录\",\n opening: \"期初余额\",\n accounts: \"科目\",\n ledger: \"明细账\",\n balanceSheet: \"资产负债表\",\n profitLoss: \"损益\",\n settings: \"设置\",\n },\n bookSwitcher: {\n label: \"账簿\",\n newBook: \"新建账簿…\",\n create: \"创建\",\n nameLabel: \"名称\",\n currencyLabel: \"货币\",\n countryLabel: \"国家/地区\",\n countryPlaceholder: \"选择国家/地区…\",\n countryHint: \"国家/地区决定税务管辖,助手可据此提供本地化建议(日本T号、欧盟VAT ID等)。\",\n fiscalYearEndLabel: \"财年结束日\",\n fiscalYearEndQ1: \"3月31日 (Q1)\",\n fiscalYearEndQ2: \"6月30日 (Q2)\",\n fiscalYearEndQ3: \"9月30日 (Q3)\",\n fiscalYearEndQ4: \"12月31日 (Q4)\",\n fiscalYearEndHint: \"决定本账簿日期范围快捷方式所使用的财年边界。默认 12月31日(Q4 — 自然年)。\",\n placeholder: \"选择账簿…\",\n firstRunHint: \"请为第一本账簿选择名称、货币、国家/地区与财年结束日。货币按账簿固定,登账后再更改会比较困难。\",\n },\n deletedNotice: {\n title: \"“{bookName}” 已删除。\",\n body: \"请从上方下拉菜单选择其他账簿,或新建一本。\",\n },\n journalList: {\n fromLabel: \"起始\",\n toLabel: \"结束\",\n accountLabel: \"科目\",\n allAccounts: \"所有科目\",\n void: \"冲销\",\n edit: \"编辑\",\n voidConfirm: \"冲销此分录?原始分录保留在日记账中,会追加一对反向分录。\",\n voidReason: \"原因(可选):\",\n columns: {\n date: \"日期\",\n kind: \"类型\",\n memo: \"备注\",\n lines: \"明细\",\n },\n kind: {\n normal: \"—\",\n opening: \"期初\",\n void: \"反向\",\n voidMarker: \"冲销标记\",\n },\n },\n entryForm: {\n title: \"新建分录\",\n editTitle: \"编辑分录\",\n editBanner: \"提交后,原分录将被冲销并以此内容替换。\",\n dateLabel: \"日期\",\n memoLabel: \"备注\",\n accountLabel: \"科目\",\n debitLabel: \"借方\",\n creditLabel: \"贷方\",\n taxRegistrationIdLabel: \"税务登记号\",\n taxRegistrationIdPlaceholder: \"T-number / VAT ID / GSTIN…\",\n taxRegistrationIdMissingWarning: \"必填\",\n addLine: \"添加行\",\n removeLine: \"删除\",\n submit: \"登账\",\n submitting: \"登账中…\",\n update: \"更新\",\n updating: \"更新中…\",\n cancelEdit: \"取消\",\n success: \"已登账。\",\n editSuccess: \"分录已更新。\",\n editVoidReason: \"已编辑\",\n imbalance: \"差额: {amount}\",\n balanced: \"已平衡 ✓\",\n },\n openingForm: {\n title: \"期初余额\",\n asOfLabel: \"基准日\",\n explainer: \"仅允许 B/S 科目(资产 / 负债 / 权益)。Σ借方=Σ贷方,差额会落入 Retained Earnings。\",\n emptyHint: \"可以全部留空保存——期初余额稍后再更新。账簿只需先存在一条期初记录,其他标签页才会解锁。\",\n explainer2: \"仅 B/S 科目。\",\n submit: \"保存期初余额\",\n replaceWarning: \"保存会替换现有期初余额(旧的会在日记账中冲销)。\",\n none: \"尚未设置期初余额。\",\n setBy: \"{date} 已设置\",\n success: \"期初余额已保存。\",\n },\n ledger: {\n selectAccount: \"选择科目\",\n closingBalance: \"期末余额\",\n columns: {\n date: \"日期\",\n memo: \"备注\",\n debit: \"借方\",\n credit: \"贷方\",\n balance: \"余额\",\n taxRegistrationId: \"税务登记号\",\n },\n },\n dateRange: {\n shortcutLabel: \"范围\",\n currentQuarter: \"本季度\",\n previousQuarter: \"上季度\",\n currentYear: \"本年度\",\n previousYear: \"上年度\",\n lifetime: \"自开账以来\",\n all: \"全部\",\n fromLabel: \"起始\",\n toLabel: \"结束\",\n },\n balanceSheet: {\n asOfLabel: \"期间\",\n sections: {\n asset: \"资产\",\n liability: \"负债\",\n equity: \"权益\",\n },\n total: \"合计\",\n imbalance: \"差额: {amount}\",\n currentEarnings: \"当期净损益\",\n shortcutLabel: \"快捷方式\",\n thisMonth: \"本月\",\n lastMonth: \"上月\",\n lastQuarter: \"上季度\",\n lastYear: \"上年\",\n },\n profitLoss: {\n fromLabel: \"起始\",\n toLabel: \"结束\",\n income: \"收入\",\n expense: \"支出\",\n netIncome: \"净利润:\",\n },\n accounts: {\n listEmpty: \"此分类下暂无科目。\",\n openLedgerAria: \"打开 {code} {name} 的明细账\",\n manageButton: \"管理科目\",\n modalTitle: \"管理科目\",\n addAccount: \"添加科目\",\n sectionTitle: {\n asset: \"资产\",\n liability: \"负债\",\n equity: \"权益\",\n income: \"收入\",\n expense: \"费用\",\n },\n columnCode: \"代码\",\n columnName: \"名称\",\n columnType: \"类型\",\n columnNote: \"备注\",\n typeOption: {\n asset: \"资产\",\n liability: \"负债\",\n equity: \"权益\",\n income: \"收入\",\n expense: \"费用\",\n },\n edit: \"编辑\",\n save: \"保存\",\n cancel: \"取消\",\n saving: \"保存中…\",\n addToCategory: \"添加{type}\",\n deactivate: \"停用\",\n reactivate: \"启用\",\n deactivateConfirm: \"在分录/明细账下拉框中隐藏“{name}”?现有日记账分录不受影响。\",\n errorEmptyCode: \"代码不能为空。\",\n errorReservedCode: \"以 “_” 开头的代码保留给系统行使用。\",\n errorInvalidCodeFormat: \"科目代码必须为 4 位数字。\",\n errorCodeTypeMismatch: \"代码首位必须与科目类型匹配:1xxx 资产,2xxx 负债,3xxx 权益,4xxx 收入,5xxx 费用。\",\n errorEmptyName: \"名称不能为空。\",\n errorDuplicateCode: \"此代码的科目已存在。\",\n errorDuplicateName: \"此名称的科目已存在。\",\n success: \"科目已保存。\",\n codeReadOnlyHint: \"科目创建后代码不能更改。\",\n noteOptional: \"(可选)\",\n },\n settings: {\n bookInfo: \"账簿信息\",\n bookInfoExplain: \"修改国家/地区可更新税务管辖相关建议。开始登账后货币不可更改。\",\n countryUnset: \"(未设置)\",\n fiscalYearEndExplain: \"仅影响今后日期范围快捷方式的解析方式;现有日记账分录不会移动。\",\n saveChanges: \"保存更改\",\n updateOk: \"账簿已更新。\",\n rebuild: \"重建快照\",\n rebuildExplain: \"删除全部月度快照缓存并从日记账重新计算。手动编辑日记账后可用。\",\n rebuildOk: \"已重建 {count} 期。\",\n advanced: \"高级设置…\",\n deleteBook: \"删除账簿\",\n deleteBookExplain: \"永久删除该账簿目录。无法撤销。\",\n deleteBookConfirm: '请输入 \"{bookName}\" 以确认:',\n deleteBookButton: \"彻底删除\",\n },\n preview: {\n entry: \"已记账 {date} 的分录\",\n pl: \"P&L {from} → {to}: 净额 {net}\",\n bs: \"{date} 资产负债表 · 资产 {assets}\",\n bookCreated: '已创建账簿 \"{name}\" ({id})',\n },\n previewSummary: \"会计 · {bookId}\",\n previewError: \"会计: {error}\",\n previewGeneric: \"会计结果\",\n },\n};\n\nexport default zhMessages;\n","// Auto-extracted from the host src/lang/ko.ts during the accounting-plugin\n// i18n migration. The plugin owns its own copy so it uses NO host i18n resources.\nimport type { AccountingMessages } from \"./en\";\n\nconst koMessages: AccountingMessages = {\n pluginAccounting: {\n title: \"회계\",\n noBook: '장부가 없습니다 — 위의 \"+ 새 장부\"를 눌러 첫 장부를 만드세요.',\n common: {\n cancel: \"취소\",\n loading: \"불러오는 중…\",\n error: \"오류: {error}\",\n empty: \"아직 항목이 없습니다.\",\n },\n tabs: {\n journal: \"분개장\",\n newEntry: \"새 분개\",\n opening: \"기초잔액\",\n accounts: \"계정\",\n ledger: \"원장\",\n balanceSheet: \"재무상태표\",\n profitLoss: \"손익\",\n settings: \"설정\",\n },\n bookSwitcher: {\n label: \"장부\",\n newBook: \"새 장부…\",\n create: \"생성\",\n nameLabel: \"이름\",\n currencyLabel: \"통화\",\n countryLabel: \"국가\",\n countryPlaceholder: \"국가 선택…\",\n countryHint: \"국가를 설정하면 세무 관할이 결정되어 어시스턴트가 국가별 안내(일본 T-번호, EU VAT ID 등)를 제공할 수 있습니다.\",\n fiscalYearEndLabel: \"회계연도 종료일\",\n fiscalYearEndQ1: \"3월 31일 (Q1)\",\n fiscalYearEndQ2: \"6월 30일 (Q2)\",\n fiscalYearEndQ3: \"9월 30일 (Q3)\",\n fiscalYearEndQ4: \"12월 31일 (Q4)\",\n fiscalYearEndHint: \"이 장부의 날짜 범위 단축 메뉴에 사용되는 회계연도 경계를 결정합니다. 기본값은 12월 31일(Q4 — 달력 연도)입니다.\",\n placeholder: \"장부 선택…\",\n firstRunHint: \"첫 장부의 이름, 통화, 국가, 회계연도 종료일을 선택하세요. 통화는 장부별로 설정되며 분개를 시작한 뒤에는 변경이 어렵습니다.\",\n },\n deletedNotice: {\n title: \"“{bookName}” 장부가 삭제되었습니다.\",\n body: \"위 드롭다운에서 다른 장부를 선택하거나 새 장부를 만드세요.\",\n },\n journalList: {\n fromLabel: \"시작\",\n toLabel: \"종료\",\n accountLabel: \"계정\",\n allAccounts: \"모든 계정\",\n void: \"취소\",\n edit: \"편집\",\n voidConfirm: \"이 분개를 취소하시겠습니까? 원 분개는 분개장에 남고 반대 분개가 추가됩니다.\",\n voidReason: \"사유(선택):\",\n columns: {\n date: \"날짜\",\n kind: \"종류\",\n memo: \"메모\",\n lines: \"라인\",\n },\n kind: {\n normal: \"—\",\n opening: \"기초\",\n void: \"반대\",\n voidMarker: \"취소 마커\",\n },\n },\n entryForm: {\n title: \"새 분개\",\n editTitle: \"분개 편집\",\n editBanner: \"제출하면 원본 분개가 무효 처리되고 이 내용으로 대체됩니다.\",\n dateLabel: \"날짜\",\n memoLabel: \"메모\",\n accountLabel: \"계정\",\n debitLabel: \"차변\",\n creditLabel: \"대변\",\n taxRegistrationIdLabel: \"세무 등록번호\",\n taxRegistrationIdPlaceholder: \"T-number / VAT ID / GSTIN…\",\n taxRegistrationIdMissingWarning: \"필수\",\n addLine: \"라인 추가\",\n removeLine: \"삭제\",\n submit: \"등록\",\n submitting: \"등록 중…\",\n update: \"업데이트\",\n updating: \"업데이트 중…\",\n cancelEdit: \"취소\",\n success: \"분개를 등록했습니다.\",\n editSuccess: \"분개가 업데이트되었습니다.\",\n editVoidReason: \"편집됨\",\n imbalance: \"차액: {amount}\",\n balanced: \"균형 ✓\",\n },\n openingForm: {\n title: \"기초잔액\",\n asOfLabel: \"기준일\",\n explainer: \"재무상태표 계정(자산/부채/자본)만 입력 가능. Σ차변=Σ대변이 되어야 하며 차액은 Retained Earnings에 흡수됩니다.\",\n emptyHint:\n \"모두 비워둔 채 저장해도 괜찮습니다 — 기초잔액은 나중에 업데이트할 수 있습니다. 다른 탭을 사용하려면 일단 기초잔액 항목이 등록되어 있어야 합니다.\",\n explainer2: \"재무상태표 계정만.\",\n submit: \"기초잔액 저장\",\n replaceWarning: \"저장 시 기존 기초잔액은 분개장에서 취소됩니다.\",\n none: \"기초잔액이 설정되지 않았습니다.\",\n setBy: \"{date} 기준 설정됨\",\n success: \"기초잔액을 저장했습니다.\",\n },\n ledger: {\n selectAccount: \"계정 선택\",\n closingBalance: \"기말 잔액\",\n columns: {\n date: \"날짜\",\n memo: \"메모\",\n debit: \"차변\",\n credit: \"대변\",\n balance: \"잔액\",\n taxRegistrationId: \"세무 등록번호\",\n },\n },\n dateRange: {\n shortcutLabel: \"범위\",\n currentQuarter: \"이번 분기\",\n previousQuarter: \"지난 분기\",\n currentYear: \"이번 연도\",\n previousYear: \"지난 연도\",\n lifetime: \"개설 이후\",\n all: \"전체\",\n fromLabel: \"시작\",\n toLabel: \"종료\",\n },\n balanceSheet: {\n asOfLabel: \"기간\",\n sections: {\n asset: \"자산\",\n liability: \"부채\",\n equity: \"자본\",\n },\n total: \"합계\",\n imbalance: \"차액: {amount}\",\n currentEarnings: \"당기 순손익\",\n shortcutLabel: \"바로가기\",\n thisMonth: \"이번 달\",\n lastMonth: \"지난달\",\n lastQuarter: \"지난 분기\",\n lastYear: \"지난 연도\",\n },\n profitLoss: {\n fromLabel: \"시작\",\n toLabel: \"종료\",\n income: \"수익\",\n expense: \"비용\",\n netIncome: \"당기순이익:\",\n },\n accounts: {\n listEmpty: \"이 카테고리에는 아직 계정이 없습니다.\",\n openLedgerAria: \"{code} {name} 원장 열기\",\n manageButton: \"계정 관리\",\n modalTitle: \"계정 관리\",\n addAccount: \"계정 추가\",\n sectionTitle: {\n asset: \"자산\",\n liability: \"부채\",\n equity: \"자본\",\n income: \"수익\",\n expense: \"비용\",\n },\n columnCode: \"코드\",\n columnName: \"이름\",\n columnType: \"유형\",\n columnNote: \"메모\",\n typeOption: {\n asset: \"자산\",\n liability: \"부채\",\n equity: \"자본\",\n income: \"수익\",\n expense: \"비용\",\n },\n edit: \"편집\",\n save: \"저장\",\n cancel: \"취소\",\n saving: \"저장 중…\",\n addToCategory: \"{type} 추가\",\n deactivate: \"비활성화\",\n reactivate: \"활성화\",\n deactivateConfirm: \"‘{name}’을(를) 분개/원장 드롭다운에서 숨기시겠습니까? 기존 분개 항목에는 영향이 없습니다.\",\n errorEmptyCode: \"코드는 필수입니다.\",\n errorReservedCode: \"“_”로 시작하는 코드는 시스템 행에 예약되어 있습니다.\",\n errorInvalidCodeFormat: \"계정 코드는 정확히 4자리 숫자여야 합니다.\",\n errorCodeTypeMismatch: \"코드의 첫 자리는 계정 유형과 일치해야 합니다: 1xxx 자산, 2xxx 부채, 3xxx 자본, 4xxx 수익, 5xxx 비용.\",\n errorEmptyName: \"이름은 필수입니다.\",\n errorDuplicateCode: \"이 코드의 계정이 이미 존재합니다.\",\n errorDuplicateName: \"이 이름의 계정이 이미 존재합니다.\",\n success: \"계정을 저장했습니다.\",\n codeReadOnlyHint: \"계정을 만든 후에는 코드를 변경할 수 없습니다.\",\n noteOptional: \"(선택)\",\n },\n settings: {\n bookInfo: \"장부 정보\",\n bookInfoExplain: \"국가를 변경하면 세무 관할 기반 안내가 갱신됩니다. 분개를 시작한 뒤에는 통화를 변경할 수 없습니다.\",\n countryUnset: \"(미설정)\",\n fiscalYearEndExplain: \"이후 날짜 범위 단축 메뉴의 해석 방식만 변경됩니다. 기존 분개 항목은 이동되지 않습니다.\",\n saveChanges: \"변경 저장\",\n updateOk: \"장부를 업데이트했습니다.\",\n rebuild: \"스냅샷 재구축\",\n rebuildExplain: \"모든 월별 스냅샷 캐시를 지우고 분개장에서 재계산합니다. 분개 파일을 직접 편집한 뒤 복구할 때 유용합니다.\",\n rebuildOk: \"{count}개 기간을 재구축했습니다.\",\n advanced: \"고급…\",\n deleteBook: \"장부 삭제\",\n deleteBookExplain: \"이 장부의 디렉터리를 영구히 삭제합니다. 되돌릴 수 없습니다.\",\n deleteBookConfirm: '확인을 위해 \"{bookName}\"을 입력하세요:',\n deleteBookButton: \"영구 삭제\",\n },\n preview: {\n entry: \"{date} 분개를 등록\",\n pl: \"P&L {from} → {to}: 순이익 {net}\",\n bs: \"{date} 재무상태표 · 자산 {assets}\",\n bookCreated: '장부 \"{name}\" ({id})를 생성했습니다',\n },\n previewSummary: \"회계 · {bookId}\",\n previewError: \"회계: {error}\",\n previewGeneric: \"회계 결과\",\n },\n};\n\nexport default koMessages;\n","// Auto-extracted from the host src/lang/es.ts during the accounting-plugin\n// i18n migration. The plugin owns its own copy so it uses NO host i18n resources.\nimport type { AccountingMessages } from \"./en\";\n\nconst esMessages: AccountingMessages = {\n pluginAccounting: {\n title: \"Contabilidad\",\n noBook: 'Aún no hay libros — pulsa \"+ Nuevo libro\" arriba para crear el primero.',\n common: {\n cancel: \"Cancelar\",\n loading: \"Cargando…\",\n error: \"Error: {error}\",\n empty: \"Aún no hay entradas.\",\n },\n tabs: {\n journal: \"Diario\",\n newEntry: \"Nueva entrada\",\n opening: \"Apertura\",\n accounts: \"Cuentas\",\n ledger: \"Mayor\",\n balanceSheet: \"Balance\",\n profitLoss: \"P&L\",\n settings: \"Ajustes\",\n },\n bookSwitcher: {\n label: \"Libro\",\n newBook: \"Nuevo libro…\",\n create: \"Crear\",\n nameLabel: \"Nombre\",\n currencyLabel: \"Moneda\",\n countryLabel: \"País\",\n countryPlaceholder: \"Selecciona un país…\",\n countryHint: \"El país define la jurisdicción fiscal para que el asistente pueda dar consejos específicos (T-number en Japón, ID de IVA en la UE, etc.).\",\n fiscalYearEndLabel: \"Cierre del ejercicio\",\n fiscalYearEndQ1: \"31 de marzo (Q1)\",\n fiscalYearEndQ2: \"30 de junio (Q2)\",\n fiscalYearEndQ3: \"30 de septiembre (Q3)\",\n fiscalYearEndQ4: \"31 de diciembre (Q4)\",\n fiscalYearEndHint: \"Define el límite del ejercicio para los atajos de rango de fechas de este libro. Por defecto: 31 de diciembre (Q4 — año natural).\",\n placeholder: \"Elige un libro…\",\n firstRunHint:\n \"Elige el nombre, la moneda, el país y el cierre de ejercicio de tu primer libro. La moneda se fija por libro y es difícil cambiarla una vez que empieces a registrar asientos.\",\n },\n deletedNotice: {\n title: \"“{bookName}” se ha eliminado.\",\n body: \"Elige otro libro en el desplegable de arriba o crea uno nuevo.\",\n },\n journalList: {\n fromLabel: \"Desde\",\n toLabel: \"Hasta\",\n accountLabel: \"Cuenta\",\n allAccounts: \"Todas\",\n void: \"Anular\",\n edit: \"Editar\",\n voidConfirm: \"¿Anular esta entrada? La original permanece en el diario; se añade un par de reversión.\",\n voidReason: \"Motivo (opcional):\",\n columns: {\n date: \"Fecha\",\n kind: \"Tipo\",\n memo: \"Memo\",\n lines: \"Líneas\",\n },\n kind: {\n normal: \"—\",\n opening: \"Apertura\",\n void: \"Reverso\",\n voidMarker: \"Marcador de anulación\",\n },\n },\n entryForm: {\n title: \"Nueva entrada\",\n editTitle: \"Editar entrada\",\n editBanner: \"Al enviar, la entrada original será anulada y sustituida por esta.\",\n dateLabel: \"Fecha\",\n memoLabel: \"Memo\",\n accountLabel: \"Cuenta\",\n debitLabel: \"Debe\",\n creditLabel: \"Haber\",\n taxRegistrationIdLabel: \"ID fiscal\",\n taxRegistrationIdPlaceholder: \"NIF / VAT ID / RFC…\",\n taxRegistrationIdMissingWarning: \"Obligatorio\",\n addLine: \"Añadir línea\",\n removeLine: \"Quitar\",\n submit: \"Asentar\",\n submitting: \"Asentando…\",\n update: \"Actualizar\",\n updating: \"Actualizando…\",\n cancelEdit: \"Cancelar\",\n success: \"Entrada asentada.\",\n editSuccess: \"Entrada actualizada.\",\n editVoidReason: \"editado\",\n imbalance: \"Descuadre: {amount}\",\n balanced: \"Cuadrada ✓\",\n },\n openingForm: {\n title: \"Saldos de apertura\",\n asOfLabel: \"Al\",\n explainer: \"Solo cuentas de balance (activo/pasivo/patrimonio). Σ debe = Σ haber; la diferencia va a Retained Earnings.\",\n emptyHint:\n \"Puedes guardar todo en blanco — luego puedes actualizar los saldos de apertura. Solo hace falta tener una apertura registrada para desbloquear las demás pestañas.\",\n explainer2: \"Solo cuentas de balance.\",\n submit: \"Guardar saldos de apertura\",\n replaceWarning: \"Al guardar se reemplaza la apertura existente (la anterior se anula en el diario).\",\n none: \"Aún no hay saldos de apertura.\",\n setBy: \"Establecido al {date}\",\n success: \"Saldos guardados.\",\n },\n ledger: {\n selectAccount: \"Seleccionar cuenta\",\n closingBalance: \"Saldo final\",\n columns: {\n date: \"Fecha\",\n memo: \"Memo\",\n debit: \"Debe\",\n credit: \"Haber\",\n balance: \"Saldo\",\n taxRegistrationId: \"ID fiscal\",\n },\n },\n dateRange: {\n shortcutLabel: \"Rango\",\n currentQuarter: \"Trimestre actual\",\n previousQuarter: \"Último trimestre\",\n currentYear: \"Año en curso\",\n previousYear: \"Año anterior\",\n lifetime: \"Desde la apertura\",\n all: \"Todo\",\n fromLabel: \"Desde\",\n toLabel: \"Hasta\",\n },\n balanceSheet: {\n asOfLabel: \"Período\",\n sections: {\n asset: \"Activo\",\n liability: \"Pasivo\",\n equity: \"Patrimonio\",\n },\n total: \"Total\",\n imbalance: \"Descuadre: {amount}\",\n currentEarnings: \"Resultado del período\",\n shortcutLabel: \"Atajo\",\n thisMonth: \"Este mes\",\n lastMonth: \"Mes anterior\",\n lastQuarter: \"Último trimestre\",\n lastYear: \"Año anterior\",\n },\n profitLoss: {\n fromLabel: \"Desde\",\n toLabel: \"Hasta\",\n income: \"Ingresos\",\n expense: \"Gastos\",\n netIncome: \"Resultado neto:\",\n },\n accounts: {\n listEmpty: \"No hay cuentas en esta categoría.\",\n openLedgerAria: \"Abrir mayor de {code} {name}\",\n manageButton: \"Gestionar cuentas\",\n modalTitle: \"Gestionar cuentas\",\n addAccount: \"Añadir cuenta\",\n sectionTitle: {\n asset: \"Activos\",\n liability: \"Pasivos\",\n equity: \"Patrimonio\",\n income: \"Ingresos\",\n expense: \"Gastos\",\n },\n columnCode: \"Código\",\n columnName: \"Nombre\",\n columnType: \"Tipo\",\n columnNote: \"Nota\",\n typeOption: {\n asset: \"Activo\",\n liability: \"Pasivo\",\n equity: \"Patrimonio\",\n income: \"Ingreso\",\n expense: \"Gasto\",\n },\n edit: \"Editar\",\n save: \"Guardar\",\n cancel: \"Cancelar\",\n saving: \"Guardando…\",\n addToCategory: \"Añadir cuenta de {type}\",\n deactivate: \"Desactivar\",\n reactivate: \"Activar\",\n deactivateConfirm: \"¿Ocultar “{name}” de los desplegables de asientos / mayor? Los asientos existentes no se ven afectados.\",\n errorEmptyCode: \"El código es obligatorio.\",\n errorReservedCode: \"Los códigos que empiezan por “_” están reservados para filas del sistema.\",\n errorInvalidCodeFormat: \"El código de cuenta debe tener exactamente 4 dígitos.\",\n errorCodeTypeMismatch: \"El código debe coincidir con el tipo de cuenta: 1xxx activo, 2xxx pasivo, 3xxx patrimonio, 4xxx ingreso, 5xxx gasto.\",\n errorEmptyName: \"El nombre es obligatorio.\",\n errorDuplicateCode: \"Ya existe una cuenta con este código.\",\n errorDuplicateName: \"Ya existe una cuenta con este nombre.\",\n success: \"Cuenta guardada.\",\n codeReadOnlyHint: \"El código no puede cambiarse una vez creada la cuenta.\",\n noteOptional: \"(opcional)\",\n },\n settings: {\n bookInfo: \"Información del libro\",\n bookInfoExplain:\n \"Edita el país para actualizar los consejos según la jurisdicción fiscal. La moneda no puede cambiarse una vez registrados los asientos.\",\n countryUnset: \"(sin definir)\",\n fiscalYearEndExplain: \"Solo cambia cómo se resuelven los atajos de rango de fechas en adelante; los asientos existentes no se mueven.\",\n saveChanges: \"Guardar cambios\",\n updateOk: \"Libro actualizado.\",\n rebuild: \"Reconstruir snapshots\",\n rebuildExplain: \"Borra todos los snapshots mensuales y los recalcula desde el diario. Útil tras editar el diario a mano.\",\n rebuildOk: \"Reconstruidos {count} período(s).\",\n advanced: \"Avanzado…\",\n deleteBook: \"Eliminar libro\",\n deleteBookExplain: \"Elimina permanentemente el directorio del libro. No se puede deshacer.\",\n deleteBookConfirm: 'Escribe \"{bookName}\" para confirmar:',\n deleteBookButton: \"Eliminar para siempre\",\n },\n preview: {\n entry: \"Entrada asentada el {date}\",\n pl: \"P&L {from} → {to}: neto {net}\",\n bs: \"Balance al {date} · activos {assets}\",\n bookCreated: 'Libro \"{name}\" ({id}) creado',\n },\n previewSummary: \"Contabilidad · {bookId}\",\n previewError: \"Contabilidad: {error}\",\n previewGeneric: \"Resultado de contabilidad\",\n },\n};\n\nexport default esMessages;\n","// Auto-extracted from the host src/lang/pt-BR.ts during the accounting-plugin\n// i18n migration. The plugin owns its own copy so it uses NO host i18n resources.\nimport type { AccountingMessages } from \"./en\";\n\nconst ptBRMessages: AccountingMessages = {\n pluginAccounting: {\n title: \"Contabilidade\",\n noBook: 'Ainda não há livros — clique em \"+ Novo livro\" acima para criar o primeiro.',\n common: {\n cancel: \"Cancelar\",\n loading: \"Carregando…\",\n error: \"Erro: {error}\",\n empty: \"Ainda não há lançamentos.\",\n },\n tabs: {\n journal: \"Diário\",\n newEntry: \"Novo lançamento\",\n opening: \"Abertura\",\n accounts: \"Contas\",\n ledger: \"Razão\",\n balanceSheet: \"Balanço\",\n profitLoss: \"DRE\",\n settings: \"Configurações\",\n },\n bookSwitcher: {\n label: \"Livro\",\n newBook: \"Novo livro…\",\n create: \"Criar\",\n nameLabel: \"Nome\",\n currencyLabel: \"Moeda\",\n countryLabel: \"País\",\n countryPlaceholder: \"Selecione um país…\",\n countryHint: \"O país define a jurisdição fiscal para que o assistente possa dar conselhos específicos (T-number no Japão, ID de IVA na UE, etc.).\",\n fiscalYearEndLabel: \"Encerramento do exercício\",\n fiscalYearEndQ1: \"31 de março (Q1)\",\n fiscalYearEndQ2: \"30 de junho (Q2)\",\n fiscalYearEndQ3: \"30 de setembro (Q3)\",\n fiscalYearEndQ4: \"31 de dezembro (Q4)\",\n fiscalYearEndHint: \"Define o limite do exercício social usado pelos atalhos de período deste livro. Padrão: 31 de dezembro (Q4 — ano civil).\",\n placeholder: \"Selecione um livro…\",\n firstRunHint:\n \"Escolha o nome, a moeda, o país e o encerramento do exercício do seu primeiro livro. A moeda é definida por livro e fica difícil alterar depois de começar a lançar.\",\n },\n deletedNotice: {\n title: \"“{bookName}” foi excluído.\",\n body: \"Selecione outro livro no menu acima ou crie um novo.\",\n },\n journalList: {\n fromLabel: \"De\",\n toLabel: \"Até\",\n accountLabel: \"Conta\",\n allAccounts: \"Todas as contas\",\n void: \"Estornar\",\n edit: \"Editar\",\n voidConfirm: \"Estornar este lançamento? O original permanece no diário e um par de reversão é anexado.\",\n voidReason: \"Motivo (opcional):\",\n columns: {\n date: \"Data\",\n kind: \"Tipo\",\n memo: \"Memo\",\n lines: \"Linhas\",\n },\n kind: {\n normal: \"—\",\n opening: \"Abertura\",\n void: \"Reverso\",\n voidMarker: \"Marcador de estorno\",\n },\n },\n entryForm: {\n title: \"Novo lançamento\",\n editTitle: \"Editar lançamento\",\n editBanner: \"Ao enviar, o lançamento original será estornado e substituído por este.\",\n dateLabel: \"Data\",\n memoLabel: \"Memo\",\n accountLabel: \"Conta\",\n debitLabel: \"Débito\",\n creditLabel: \"Crédito\",\n taxRegistrationIdLabel: \"Inscrição fiscal\",\n taxRegistrationIdPlaceholder: \"CNPJ / VAT ID / GSTIN…\",\n taxRegistrationIdMissingWarning: \"Obrigatório\",\n addLine: \"Adicionar linha\",\n removeLine: \"Remover\",\n submit: \"Lançar\",\n submitting: \"Lançando…\",\n update: \"Atualizar\",\n updating: \"Atualizando…\",\n cancelEdit: \"Cancelar\",\n success: \"Lançamento registrado.\",\n editSuccess: \"Lançamento atualizado.\",\n editVoidReason: \"editado\",\n imbalance: \"Diferença: {amount}\",\n balanced: \"Equilibrado ✓\",\n },\n openingForm: {\n title: \"Saldos iniciais\",\n asOfLabel: \"Em\",\n explainer: \"Apenas contas de balanço (ativo/passivo/patrimônio). Σ débito = Σ crédito; a diferença entra em Retained Earnings.\",\n emptyHint:\n \"Você pode salvar tudo em branco — os saldos iniciais podem ser atualizados depois. Basta ter uma abertura registrada para que as outras abas sejam liberadas.\",\n explainer2: \"Apenas contas de balanço.\",\n submit: \"Salvar saldos iniciais\",\n replaceWarning: \"Salvar substitui a abertura existente (a anterior é estornada no diário).\",\n none: \"Ainda não há saldo inicial.\",\n setBy: \"Definido em {date}\",\n success: \"Saldos iniciais salvos.\",\n },\n ledger: {\n selectAccount: \"Selecionar conta\",\n closingBalance: \"Saldo final\",\n columns: {\n date: \"Data\",\n memo: \"Memo\",\n debit: \"Débito\",\n credit: \"Crédito\",\n balance: \"Saldo\",\n taxRegistrationId: \"Inscrição fiscal\",\n },\n },\n dateRange: {\n shortcutLabel: \"Período\",\n currentQuarter: \"Trimestre atual\",\n previousQuarter: \"Último trimestre\",\n currentYear: \"Ano atual\",\n previousYear: \"Ano anterior\",\n lifetime: \"Desde a abertura\",\n all: \"Tudo\",\n fromLabel: \"De\",\n toLabel: \"Até\",\n },\n balanceSheet: {\n asOfLabel: \"Período\",\n sections: {\n asset: \"Ativo\",\n liability: \"Passivo\",\n equity: \"Patrimônio\",\n },\n total: \"Total\",\n imbalance: \"Diferença: {amount}\",\n currentEarnings: \"Resultado do período\",\n shortcutLabel: \"Atalho\",\n thisMonth: \"Este mês\",\n lastMonth: \"Mês anterior\",\n lastQuarter: \"Último trimestre\",\n lastYear: \"Ano anterior\",\n },\n profitLoss: {\n fromLabel: \"De\",\n toLabel: \"Até\",\n income: \"Receita\",\n expense: \"Despesa\",\n netIncome: \"Resultado líquido:\",\n },\n accounts: {\n listEmpty: \"Ainda não há contas nesta categoria.\",\n openLedgerAria: \"Abrir razão de {code} {name}\",\n manageButton: \"Gerenciar contas\",\n modalTitle: \"Gerenciar contas\",\n addAccount: \"Adicionar conta\",\n sectionTitle: {\n asset: \"Ativos\",\n liability: \"Passivos\",\n equity: \"Patrimônio\",\n income: \"Receitas\",\n expense: \"Despesas\",\n },\n columnCode: \"Código\",\n columnName: \"Nome\",\n columnType: \"Tipo\",\n columnNote: \"Observação\",\n typeOption: {\n asset: \"Ativo\",\n liability: \"Passivo\",\n equity: \"Patrimônio\",\n income: \"Receita\",\n expense: \"Despesa\",\n },\n edit: \"Editar\",\n save: \"Salvar\",\n cancel: \"Cancelar\",\n saving: \"Salvando…\",\n addToCategory: \"Adicionar conta de {type}\",\n deactivate: \"Desativar\",\n reactivate: \"Ativar\",\n deactivateConfirm: \"Ocultar “{name}” dos menus de lançamentos / razão? Os lançamentos existentes não são afetados.\",\n errorEmptyCode: \"O código é obrigatório.\",\n errorReservedCode: \"Códigos iniciados por “_” são reservados para linhas do sistema.\",\n errorInvalidCodeFormat: \"O código da conta deve ter exatamente 4 dígitos.\",\n errorCodeTypeMismatch: \"O código deve corresponder ao tipo de conta: 1xxx ativo, 2xxx passivo, 3xxx patrimônio, 4xxx receita, 5xxx despesa.\",\n errorEmptyName: \"O nome é obrigatório.\",\n errorDuplicateCode: \"Já existe uma conta com este código.\",\n errorDuplicateName: \"Já existe uma conta com este nome.\",\n success: \"Conta salva.\",\n codeReadOnlyHint: \"O código não pode ser alterado depois que a conta é criada.\",\n noteOptional: \"(opcional)\",\n },\n settings: {\n bookInfo: \"Informações do livro\",\n bookInfoExplain: \"Edite o país para atualizar as recomendações conforme a jurisdição fiscal. A moeda não pode ser alterada após registrar lançamentos.\",\n countryUnset: \"(não definido)\",\n fiscalYearEndExplain: \"Apenas altera como os atalhos de período são resolvidos a partir de agora; lançamentos existentes não são movidos.\",\n saveChanges: \"Salvar alterações\",\n updateOk: \"Livro atualizado.\",\n rebuild: \"Reconstruir snapshots\",\n rebuildExplain: \"Apaga todos os snapshots mensais e recalcula a partir do diário. Útil após editar o diário manualmente.\",\n rebuildOk: \"Reconstruído(s) {count} período(s).\",\n advanced: \"Avançado…\",\n deleteBook: \"Excluir livro\",\n deleteBookExplain: \"Exclui permanentemente o diretório do livro. Não pode ser desfeito.\",\n deleteBookConfirm: 'Digite \"{bookName}\" para confirmar:',\n deleteBookButton: \"Excluir para sempre\",\n },\n preview: {\n entry: \"Lançamento em {date}\",\n pl: \"P&L {from} → {to}: líquido {net}\",\n bs: \"Balanço em {date} · ativos {assets}\",\n bookCreated: 'Livro \"{name}\" ({id}) criado',\n },\n previewSummary: \"Contabilidade · {bookId}\",\n previewError: \"Contabilidade: {error}\",\n previewGeneric: \"Resultado da contabilidade\",\n },\n};\n\nexport default ptBRMessages;\n","// Auto-extracted from the host src/lang/fr.ts during the accounting-plugin\n// i18n migration. The plugin owns its own copy so it uses NO host i18n resources.\nimport type { AccountingMessages } from \"./en\";\n\nconst frMessages: AccountingMessages = {\n pluginAccounting: {\n title: \"Comptabilité\",\n noBook: 'Pas encore de livres — cliquez sur \"+ Nouveau livre\" en haut pour créer le premier.',\n common: {\n cancel: \"Annuler\",\n loading: \"Chargement…\",\n error: \"Erreur : {error}\",\n empty: \"Aucune écriture pour l'instant.\",\n },\n tabs: {\n journal: \"Journal\",\n newEntry: \"Nouvelle écriture\",\n opening: \"Ouverture\",\n accounts: \"Comptes\",\n ledger: \"Grand livre\",\n balanceSheet: \"Bilan\",\n profitLoss: \"Résultat\",\n settings: \"Paramètres\",\n },\n bookSwitcher: {\n label: \"Livre\",\n newBook: \"Nouveau livre…\",\n create: \"Créer\",\n nameLabel: \"Nom\",\n currencyLabel: \"Devise\",\n countryLabel: \"Pays\",\n countryPlaceholder: \"Sélectionner un pays…\",\n countryHint: \"Le pays définit la juridiction fiscale afin que l'assistant puisse donner des conseils ciblés (T-number au Japon, ID TVA dans l'UE, etc.).\",\n fiscalYearEndLabel: \"Clôture de l'exercice\",\n fiscalYearEndQ1: \"31 mars (Q1)\",\n fiscalYearEndQ2: \"30 juin (Q2)\",\n fiscalYearEndQ3: \"30 septembre (Q3)\",\n fiscalYearEndQ4: \"31 décembre (Q4)\",\n fiscalYearEndHint:\n \"Définit la limite de l'exercice utilisée par les raccourcis de plage de dates de ce livre. Par défaut : 31 décembre (Q4 — année civile).\",\n placeholder: \"Sélectionner un livre…\",\n firstRunHint:\n \"Choisissez le nom, la devise, le pays et la clôture de l'exercice de votre premier livre. La devise est figée par livre et difficile à changer une fois les écritures commencées.\",\n },\n deletedNotice: {\n title: \"« {bookName} » a été supprimé.\",\n body: \"Choisissez un autre livre dans la liste ci-dessus ou créez-en un nouveau.\",\n },\n journalList: {\n fromLabel: \"Du\",\n toLabel: \"Au\",\n accountLabel: \"Compte\",\n allAccounts: \"Tous\",\n void: \"Annuler\",\n edit: \"Modifier\",\n voidConfirm: \"Annuler cette écriture ? L'originale reste dans le journal ; une paire de contre-passation est ajoutée.\",\n voidReason: \"Motif (optionnel) :\",\n columns: {\n date: \"Date\",\n kind: \"Type\",\n memo: \"Mémo\",\n lines: \"Lignes\",\n },\n kind: {\n normal: \"—\",\n opening: \"Ouverture\",\n void: \"Contre-passation\",\n voidMarker: \"Marqueur d'annulation\",\n },\n },\n entryForm: {\n title: \"Nouvelle écriture\",\n editTitle: \"Modifier l'écriture\",\n editBanner: \"À l'envoi, l'écriture d'origine sera contre-passée et remplacée par celle-ci.\",\n dateLabel: \"Date\",\n memoLabel: \"Mémo\",\n accountLabel: \"Compte\",\n debitLabel: \"Débit\",\n creditLabel: \"Crédit\",\n taxRegistrationIdLabel: \"N° d'identification fiscale\",\n taxRegistrationIdPlaceholder: \"TVA / SIRET / GSTIN…\",\n taxRegistrationIdMissingWarning: \"Obligatoire\",\n addLine: \"Ajouter une ligne\",\n removeLine: \"Supprimer\",\n submit: \"Comptabiliser\",\n submitting: \"Comptabilisation…\",\n update: \"Mettre à jour\",\n updating: \"Mise à jour…\",\n cancelEdit: \"Annuler\",\n success: \"Écriture comptabilisée.\",\n editSuccess: \"Écriture mise à jour.\",\n editVoidReason: \"modifiée\",\n imbalance: \"Déséquilibre : {amount}\",\n balanced: \"Équilibré ✓\",\n },\n openingForm: {\n title: \"Soldes d'ouverture\",\n asOfLabel: \"Au\",\n explainer: \"Comptes de bilan uniquement (actif/passif/capitaux). Σ débit = Σ crédit ; le solde tombe sur Retained Earnings.\",\n emptyHint:\n \"Vous pouvez enregistrer en laissant tout vide — les soldes d'ouverture peuvent être mis à jour plus tard. Il suffit qu'une ouverture soit enregistrée pour débloquer les autres onglets.\",\n explainer2: \"Comptes de bilan uniquement.\",\n submit: \"Enregistrer les soldes d'ouverture\",\n replaceWarning: \"Enregistrer remplace l'ouverture existante (l'ancienne est contre-passée dans le journal).\",\n none: \"Aucun solde d'ouverture défini.\",\n setBy: \"Défini au {date}\",\n success: \"Soldes d'ouverture enregistrés.\",\n },\n ledger: {\n selectAccount: \"Sélectionner un compte\",\n closingBalance: \"Solde de clôture\",\n columns: {\n date: \"Date\",\n memo: \"Mémo\",\n debit: \"Débit\",\n credit: \"Crédit\",\n balance: \"Solde\",\n taxRegistrationId: \"N° d'identification fiscale\",\n },\n },\n dateRange: {\n shortcutLabel: \"Plage\",\n currentQuarter: \"Trimestre en cours\",\n previousQuarter: \"Trimestre précédent\",\n currentYear: \"Exercice en cours\",\n previousYear: \"Exercice précédent\",\n lifetime: \"Depuis l'ouverture\",\n all: \"Tout\",\n fromLabel: \"Du\",\n toLabel: \"Au\",\n },\n balanceSheet: {\n asOfLabel: \"Période\",\n sections: {\n asset: \"Actif\",\n liability: \"Passif\",\n equity: \"Capitaux\",\n },\n total: \"Total\",\n imbalance: \"Déséquilibre : {amount}\",\n currentEarnings: \"Résultat de la période\",\n shortcutLabel: \"Raccourci\",\n thisMonth: \"Ce mois-ci\",\n lastMonth: \"Mois précédent\",\n lastQuarter: \"Trimestre précédent\",\n lastYear: \"Année précédente\",\n },\n profitLoss: {\n fromLabel: \"Du\",\n toLabel: \"Au\",\n income: \"Produits\",\n expense: \"Charges\",\n netIncome: \"Résultat net :\",\n },\n accounts: {\n listEmpty: \"Aucun compte dans cette catégorie pour l'instant.\",\n openLedgerAria: \"Ouvrir le grand livre de {code} {name}\",\n manageButton: \"Gérer les comptes\",\n modalTitle: \"Gérer les comptes\",\n addAccount: \"Ajouter un compte\",\n sectionTitle: {\n asset: \"Actifs\",\n liability: \"Passifs\",\n equity: \"Capitaux propres\",\n income: \"Produits\",\n expense: \"Charges\",\n },\n columnCode: \"Code\",\n columnName: \"Nom\",\n columnType: \"Type\",\n columnNote: \"Note\",\n typeOption: {\n asset: \"Actif\",\n liability: \"Passif\",\n equity: \"Capitaux propres\",\n income: \"Produit\",\n expense: \"Charge\",\n },\n edit: \"Modifier\",\n save: \"Enregistrer\",\n cancel: \"Annuler\",\n saving: \"Enregistrement…\",\n addToCategory: \"Ajouter un compte de {type}\",\n deactivate: \"Désactiver\",\n reactivate: \"Activer\",\n deactivateConfirm: \"Masquer « {name} » des listes déroulantes d’écriture / grand livre ? Les écritures existantes ne sont pas affectées.\",\n errorEmptyCode: \"Le code est requis.\",\n errorReservedCode: \"Les codes commençant par « _ » sont réservés aux lignes système.\",\n errorInvalidCodeFormat: \"Le code de compte doit comporter exactement 4 chiffres.\",\n errorCodeTypeMismatch: \"Le code doit correspondre au type de compte : 1xxx actif, 2xxx passif, 3xxx capitaux propres, 4xxx produit, 5xxx charge.\",\n errorEmptyName: \"Le nom est requis.\",\n errorDuplicateCode: \"Un compte avec ce code existe déjà.\",\n errorDuplicateName: \"Un compte portant ce nom existe déjà.\",\n success: \"Compte enregistré.\",\n codeReadOnlyHint: \"Le code ne peut pas être modifié une fois le compte créé.\",\n noteOptional: \"(facultatif)\",\n },\n settings: {\n bookInfo: \"Informations du livre\",\n bookInfoExplain:\n \"Modifiez le pays pour mettre à jour les conseils selon la juridiction fiscale. La devise ne peut pas être changée une fois des écritures saisies.\",\n countryUnset: \"(non défini)\",\n fiscalYearEndExplain:\n \"Modifie uniquement la résolution des raccourcis de plage de dates à partir de maintenant ; les écritures existantes ne sont pas déplacées.\",\n saveChanges: \"Enregistrer les modifications\",\n updateOk: \"Livre mis à jour.\",\n rebuild: \"Reconstruire les snapshots\",\n rebuildExplain: \"Supprime tous les snapshots mensuels et les recalcule depuis le journal. Utile après modification manuelle du journal.\",\n rebuildOk: \"{count} période(s) reconstruite(s).\",\n advanced: \"Avancé…\",\n deleteBook: \"Supprimer le livre\",\n deleteBookExplain: \"Supprime définitivement le répertoire du livre. Irréversible.\",\n deleteBookConfirm: 'Tapez \"{bookName}\" pour confirmer :',\n deleteBookButton: \"Supprimer définitivement\",\n },\n preview: {\n entry: \"Écriture comptabilisée le {date}\",\n pl: \"P&L {from} → {to} : net {net}\",\n bs: \"Bilan au {date} · actif {assets}\",\n bookCreated: 'Livre \"{name}\" ({id}) créé',\n },\n previewSummary: \"Comptabilité · {bookId}\",\n previewError: \"Comptabilité : {error}\",\n previewGeneric: \"Résultat comptable\",\n },\n};\n\nexport default frMessages;\n","// Auto-extracted from the host src/lang/de.ts during the accounting-plugin\n// i18n migration. The plugin owns its own copy so it uses NO host i18n resources.\nimport type { AccountingMessages } from \"./en\";\n\nconst deMessages: AccountingMessages = {\n pluginAccounting: {\n title: \"Buchhaltung\",\n noBook: 'Noch keine Bücher — klicken Sie oben auf \"+ Neues Buch\", um das erste anzulegen.',\n common: {\n cancel: \"Abbrechen\",\n loading: \"Lädt…\",\n error: \"Fehler: {error}\",\n empty: \"Noch keine Einträge.\",\n },\n tabs: {\n journal: \"Journal\",\n newEntry: \"Neuer Eintrag\",\n opening: \"Eröffnung\",\n accounts: \"Konten\",\n ledger: \"Hauptbuch\",\n balanceSheet: \"Bilanz\",\n profitLoss: \"GuV\",\n settings: \"Einstellungen\",\n },\n bookSwitcher: {\n label: \"Buch\",\n newBook: \"Neues Buch…\",\n create: \"Anlegen\",\n nameLabel: \"Name\",\n currencyLabel: \"Währung\",\n countryLabel: \"Land\",\n countryPlaceholder: \"Land auswählen…\",\n countryHint:\n \"Das Land legt die Steuerjurisdiktion fest, damit der Assistent länderspezifische Hinweise geben kann (T-Nummer in Japan, USt-IdNr. in der EU usw.).\",\n fiscalYearEndLabel: \"Geschäftsjahresende\",\n fiscalYearEndQ1: \"31. März (Q1)\",\n fiscalYearEndQ2: \"30. Juni (Q2)\",\n fiscalYearEndQ3: \"30. September (Q3)\",\n fiscalYearEndQ4: \"31. Dezember (Q4)\",\n fiscalYearEndHint:\n \"Bestimmt die Geschäftsjahresgrenze, die für die Datumsbereich-Schnellauswahl in diesem Buch verwendet wird. Standard: 31. Dezember (Q4 — Kalenderjahr).\",\n placeholder: \"Buch auswählen…\",\n firstRunHint:\n \"Wählen Sie Name, Währung, Land und Geschäftsjahresende für Ihr erstes Buch. Die Währung ist pro Buch fest und nach den ersten Buchungen schwer zu ändern.\",\n },\n deletedNotice: {\n title: 'Das Buch \"{bookName}\" wurde gelöscht.',\n body: \"Wählen Sie oben ein anderes Buch aus oder legen Sie ein neues an.\",\n },\n journalList: {\n fromLabel: \"Von\",\n toLabel: \"Bis\",\n accountLabel: \"Konto\",\n allAccounts: \"Alle Konten\",\n void: \"Stornieren\",\n edit: \"Bearbeiten\",\n voidConfirm: \"Diesen Eintrag stornieren? Das Original bleibt im Journal; ein Stornopaar wird angehängt.\",\n voidReason: \"Grund (optional):\",\n columns: {\n date: \"Datum\",\n kind: \"Art\",\n memo: \"Memo\",\n lines: \"Zeilen\",\n },\n kind: {\n normal: \"—\",\n opening: \"Eröffnung\",\n void: \"Storno\",\n voidMarker: \"Storno-Marker\",\n },\n },\n entryForm: {\n title: \"Neuer Eintrag\",\n editTitle: \"Eintrag bearbeiten\",\n editBanner: \"Beim Absenden wird der Originaleintrag storniert und durch diesen ersetzt.\",\n dateLabel: \"Datum\",\n memoLabel: \"Memo\",\n accountLabel: \"Konto\",\n debitLabel: \"Soll\",\n creditLabel: \"Haben\",\n taxRegistrationIdLabel: \"Steuernummer\",\n taxRegistrationIdPlaceholder: \"USt-IdNr. / VAT ID / GSTIN…\",\n taxRegistrationIdMissingWarning: \"Erforderlich\",\n addLine: \"Zeile hinzufügen\",\n removeLine: \"Entfernen\",\n submit: \"Buchen\",\n submitting: \"Bucht…\",\n update: \"Aktualisieren\",\n updating: \"Aktualisiert…\",\n cancelEdit: \"Abbrechen\",\n success: \"Eintrag gebucht.\",\n editSuccess: \"Eintrag aktualisiert.\",\n editVoidReason: \"bearbeitet\",\n imbalance: \"Differenz: {amount}\",\n balanced: \"Ausgeglichen ✓\",\n },\n openingForm: {\n title: \"Eröffnungsbilanz\",\n asOfLabel: \"Per\",\n explainer: \"Nur Bilanzkonten (Aktiva/Passiva/Eigenkapital). Σ Soll = Σ Haben; die Differenz fließt in Retained Earnings.\",\n emptyHint:\n \"Sie können das Formular leer speichern — die Eröffnungsbilanz lässt sich später aktualisieren. Es reicht, dass einmalig eine Eröffnung hinterlegt ist, damit die übrigen Tabs freigeschaltet werden.\",\n explainer2: \"Nur Bilanzkonten.\",\n submit: \"Eröffnungsbilanz speichern\",\n replaceWarning: \"Beim Speichern wird die bestehende Eröffnung ersetzt (die alte wird im Journal storniert).\",\n none: \"Noch keine Eröffnungsbilanz.\",\n setBy: \"Per {date} festgelegt\",\n success: \"Eröffnungsbilanz gespeichert.\",\n },\n ledger: {\n selectAccount: \"Konto wählen\",\n closingBalance: \"Schlusssaldo\",\n columns: {\n date: \"Datum\",\n memo: \"Memo\",\n debit: \"Soll\",\n credit: \"Haben\",\n balance: \"Saldo\",\n taxRegistrationId: \"Steuernummer\",\n },\n },\n dateRange: {\n shortcutLabel: \"Bereich\",\n currentQuarter: \"Aktuelles Quartal\",\n previousQuarter: \"Letztes Quartal\",\n currentYear: \"Aktuelles Jahr\",\n previousYear: \"Letztes Jahr\",\n lifetime: \"Seit Eröffnung\",\n all: \"Alle\",\n fromLabel: \"Von\",\n toLabel: \"Bis\",\n },\n balanceSheet: {\n asOfLabel: \"Periode\",\n sections: {\n asset: \"Aktiva\",\n liability: \"Passiva\",\n equity: \"Eigenkapital\",\n },\n total: \"Summe\",\n imbalance: \"Differenz: {amount}\",\n currentEarnings: \"Periodenergebnis\",\n shortcutLabel: \"Schnellauswahl\",\n thisMonth: \"Dieser Monat\",\n lastMonth: \"Letzter Monat\",\n lastQuarter: \"Letztes Quartal\",\n lastYear: \"Letztes Jahr\",\n },\n profitLoss: {\n fromLabel: \"Von\",\n toLabel: \"Bis\",\n income: \"Erträge\",\n expense: \"Aufwendungen\",\n netIncome: \"Jahresüberschuss:\",\n },\n accounts: {\n listEmpty: \"In dieser Kategorie sind noch keine Konten vorhanden.\",\n openLedgerAria: \"Hauptbuch für {code} {name} öffnen\",\n manageButton: \"Konten verwalten\",\n modalTitle: \"Konten verwalten\",\n addAccount: \"Konto hinzufügen\",\n sectionTitle: {\n asset: \"Aktiva\",\n liability: \"Passiva\",\n equity: \"Eigenkapital\",\n income: \"Erträge\",\n expense: \"Aufwendungen\",\n },\n columnCode: \"Code\",\n columnName: \"Name\",\n columnType: \"Typ\",\n columnNote: \"Notiz\",\n typeOption: {\n asset: \"Aktivkonto\",\n liability: \"Passivkonto\",\n equity: \"Eigenkapital\",\n income: \"Ertrag\",\n expense: \"Aufwand\",\n },\n edit: \"Bearbeiten\",\n save: \"Speichern\",\n cancel: \"Abbrechen\",\n saving: \"Wird gespeichert…\",\n addToCategory: \"Konto vom Typ {type} hinzufügen\",\n deactivate: \"Deaktivieren\",\n reactivate: \"Aktivieren\",\n deactivateConfirm: \"Soll „{name}“ aus den Buchungs- und Kontodropdowns ausgeblendet werden? Bestehende Buchungen bleiben unberührt.\",\n errorEmptyCode: \"Code ist erforderlich.\",\n errorReservedCode: \"Codes, die mit „_“ beginnen, sind für Systemzeilen reserviert.\",\n errorInvalidCodeFormat: \"Der Kontocode muss genau 4 Ziffern haben.\",\n errorCodeTypeMismatch: \"Der Code muss zur Kontoart passen: 1xxx Aktivkonto, 2xxx Passivkonto, 3xxx Eigenkapital, 4xxx Ertrag, 5xxx Aufwand.\",\n errorEmptyName: \"Name ist erforderlich.\",\n errorDuplicateCode: \"Ein Konto mit diesem Code existiert bereits.\",\n errorDuplicateName: \"Ein Konto mit diesem Namen existiert bereits.\",\n success: \"Konto gespeichert.\",\n codeReadOnlyHint: \"Der Code kann nach dem Anlegen des Kontos nicht mehr geändert werden.\",\n noteOptional: \"(optional)\",\n },\n settings: {\n bookInfo: \"Buchinformationen\",\n bookInfoExplain:\n \"Ändern Sie das Land, um die Hinweise zur Steuerjurisdiktion zu aktualisieren. Die Währung kann nach gebuchten Einträgen nicht mehr geändert werden.\",\n countryUnset: \"(nicht gesetzt)\",\n fiscalYearEndExplain: \"Ändert nur, wie die Datumsbereich-Schnellauswahl ab jetzt aufgelöst wird; bestehende Buchungen werden nicht verschoben.\",\n saveChanges: \"Änderungen speichern\",\n updateOk: \"Buch aktualisiert.\",\n rebuild: \"Snapshots neu aufbauen\",\n rebuildExplain: \"Verwirft alle Monats-Snapshots und berechnet sie aus dem Journal neu. Nützlich nach manueller Bearbeitung.\",\n rebuildOk: \"{count} Periode(n) neu aufgebaut.\",\n advanced: \"Erweitert…\",\n deleteBook: \"Buch löschen\",\n deleteBookExplain: \"Löscht das Buch-Verzeichnis dauerhaft. Nicht rückgängig zu machen.\",\n deleteBookConfirm: 'Tippen Sie \"{bookName}\" zur Bestätigung:',\n deleteBookButton: \"Endgültig löschen\",\n },\n preview: {\n entry: \"Eintrag am {date} gebucht\",\n pl: \"GuV {from} → {to}: netto {net}\",\n bs: \"Bilanz per {date} · Aktiva {assets}\",\n bookCreated: 'Buch \"{name}\" ({id}) angelegt',\n },\n previewSummary: \"Buchhaltung · {bookId}\",\n previewError: \"Buchhaltung: {error}\",\n previewGeneric: \"Buchhaltungs-Ergebnis\",\n },\n};\n\nexport default deMessages;\n","// The accounting plugin's OWN vue-i18n instance — fully self-contained, sharing\n// no i18n resources with the host. Components call `useAccountingI18n()` instead\n// of vue-i18n's `useI18n()`, so the keys (`pluginAccounting.*`) stay identical —\n// only the source changes.\n//\n// The active locale is fed through the AccountingHostContext binding\n// (`hostLocaleTag()`), not gui-chat-protocol's PLUGIN_RUNTIME_KEY: the host\n// injects it once at startup (the same DI seam as `apiCall` / `subscribe`), and\n// one detached, app-lifetime effect keeps this instance's locale in step with\n// the host's.\n\nimport { createI18n } from \"vue-i18n\";\nimport { effectScope, watchEffect } from \"vue\";\nimport { hostLocaleTag } from \"../hostContext\";\nimport enMessages, { type AccountingMessages } from \"./en\";\nimport jaMessages from \"./ja\";\nimport zhMessages from \"./zh\";\nimport koMessages from \"./ko\";\nimport esMessages from \"./es\";\nimport ptBRMessages from \"./ptBR\";\nimport frMessages from \"./fr\";\nimport deMessages from \"./de\";\n\nconst i18n = createI18n<[AccountingMessages], string, false>({\n legacy: false,\n locale: \"en\",\n fallbackLocale: \"en\",\n messages: {\n en: enMessages,\n ja: jaMessages,\n zh: zhMessages,\n ko: koMessages,\n es: esMessages,\n \"pt-BR\": ptBRMessages,\n fr: frMessages,\n de: deMessages,\n },\n});\n\nconst syncScope = effectScope(true);\nlet syncing = false;\n\n/** Mirror the host's active locale onto this instance exactly once, in a detached\n * effect so it lives for the app's lifetime rather than a single component's.\n * Called lazily on the first `useAccountingI18n()` — by then the host has called\n * `configureAccountingHost(...)`, so `hostLocaleTag()` resolves. */\nfunction ensureLocaleSync(): void {\n if (syncing) return;\n // Flip the flag only after the effect is wired — if the first locale read\n // throws (e.g. the binding isn't configured yet), a later call can retry\n // rather than being locked out forever.\n syncScope.run(() => {\n watchEffect(() => {\n i18n.global.locale.value = hostLocaleTag();\n });\n });\n syncing = true;\n}\n\n/** The plugin's i18n composable — a drop-in for vue-i18n's `useI18n()` over the\n * plugin's own self-contained instance. Returns `{ t, locale }` (destructured at\n * the call site, exactly like `useI18n()`), with `t` reading the plugin's keys\n * and `locale` the reactive tag for date/number formatting. */\nexport function useAccountingI18n(): { t: (typeof i18n.global)[\"t\"]; locale: (typeof i18n.global)[\"locale\"] } {\n ensureLocaleSync();\n return { t: i18n.global.t, locale: i18n.global.locale };\n}\n","// Typed wrapper around POST /api/accounting. Centralises the action\n// names and the response shapes so the View / sub-components don't\n// repeat the cast at every call site.\n//\n// Every helper returns `ApiResult<T>` (the discriminated union mirrored\n// in hostContext.ts) — callers pattern-match on `.ok`. There is no\n// separate error-throwing path; all surfaces (network, HTTP, app\n// validation) flow through the same shape. The actual network client is\n// host-injected (see hostContext.ts) so the package stays host-agnostic.\n\nimport { hostApiCall as apiCall, type ApiResult } from \"./hostContext\";\nimport {\n ACCOUNTING_ACTIONS,\n ACCOUNTING_API,\n type SupportedCountryCode,\n type FiscalYearEnd,\n type TimeSeriesGranularity,\n type TimeSeriesMetric,\n} from \"../shared\";\n\nexport type AccountType = \"asset\" | \"liability\" | \"equity\" | \"income\" | \"expense\";\nexport type JournalEntryKind = \"normal\" | \"opening\" | \"void\" | \"void-marker\";\n\nexport interface Account {\n code: string;\n name: string;\n type: AccountType;\n note?: string;\n /** Soft-delete flag. When `false`, the account is hidden from\n * entry/ledger dropdowns but stays visible in Manage Accounts\n * and historical entries. */\n active?: boolean;\n}\n\nexport interface JournalLine {\n accountCode: string;\n debit?: number;\n credit?: number;\n memo?: string;\n /** Counterparty's tax-authority-issued registration ID — JP\n * T-number, EU VAT ID, UK VAT registration number, GSTIN, ABN,\n * etc. See server/accounting/types.ts for the full doc. */\n taxRegistrationId?: string;\n}\n\nexport interface JournalEntry {\n id: string;\n date: string;\n kind: JournalEntryKind;\n lines: JournalLine[];\n memo?: string;\n voidedEntryId?: string;\n voidReason?: string;\n /** Set on the new entry posted via the \"edit\" flow — id of the\n * original entry that was voided in the same operation. The\n * void + new-entry pair is two sequential calls on the client,\n * not an atomic transaction. */\n replacesEntryId?: string;\n createdAt: string;\n}\n\nexport interface BookSummary {\n id: string;\n name: string;\n currency: string;\n /** ISO 3166-1 alpha-2 country code identifying the tax jurisdiction\n * the book is kept under. Constrained to `SupportedCountryCode` —\n * see `countries.ts`. Optional for backward compatibility with\n * books created before the field was introduced. */\n country?: SupportedCountryCode;\n /** Which calendar-quarter end is the book's fiscal year end:\n * Q1 → March 31, Q2 → June 30, Q3 → September 30, Q4 → December 31.\n * Optional in the persisted shape for backward compatibility —\n * read-side code treats absence as Q4 via `resolveFiscalYearEnd`.\n * See `./fiscalYear.ts`. */\n fiscalYearEnd?: FiscalYearEnd;\n createdAt: string;\n}\n\nexport interface OpenAppPayload {\n kind: \"accounting-app\";\n /** `null` when the workspace has zero books — the View renders the\n * empty state and prompts for book creation. */\n bookId: string | null;\n initialTab?: string;\n}\n\nexport interface AccountBalance {\n accountCode: string;\n netDebit: number;\n}\n\nexport interface BalanceSheetSection {\n type: AccountType;\n rows: { accountCode: string; accountName: string; balance: number }[];\n total: number;\n}\n\nexport interface BalanceSheet {\n asOf: string;\n sections: BalanceSheetSection[];\n imbalance: number;\n}\n\nexport interface ProfitLoss {\n from: string;\n to: string;\n income: { rows: { accountCode: string; accountName: string; amount: number }[]; total: number };\n expense: { rows: { accountCode: string; accountName: string; amount: number }[]; total: number };\n netIncome: number;\n}\n\nexport interface LedgerRow {\n entryId: string;\n date: string;\n kind: JournalEntryKind;\n memo?: string;\n debit: number;\n credit: number;\n runningBalance: number;\n /** Counterparty tax-registration ID per source line. The Ledger\n * view shows it as its own column when the selected account is\n * in the input-tax band (14xx — see `isTaxAccountCode`). */\n taxRegistrationId?: string;\n}\n\nexport interface Ledger {\n accountCode: string;\n accountName: string;\n rows: LedgerRow[];\n closingBalance: number;\n}\n\nexport type ReportPeriod = { kind: \"month\"; period: string } | { kind: \"range\"; from: string; to: string };\n\n// The single dispatch route this plugin owns — shared with the server\n// router via `ACCOUNTING_API` so the two can't drift.\nconst DISPATCH_URL = ACCOUNTING_API.dispatch.path;\nconst DISPATCH_METHOD = ACCOUNTING_API.dispatch.method;\n\nfunction call<T>(action: string, args: Record<string, unknown> = {}): Promise<ApiResult<T>> {\n return apiCall<T>(DISPATCH_URL, { method: DISPATCH_METHOD, body: { action, ...args } });\n}\n\n// ── Books ────────────────────────────────────────────────────────────\n\nexport function getBooks(): Promise<ApiResult<{ books: BookSummary[] }>> {\n return call(ACCOUNTING_ACTIONS.getBooks);\n}\n\nexport function createBook(input: {\n name: string;\n currency?: string;\n country?: SupportedCountryCode;\n /** Q1..Q4 — required at the form boundary, but the server silently\n * defaults absent / empty to Q4 for back-compat. */\n fiscalYearEnd?: FiscalYearEnd;\n}): Promise<ApiResult<{ book: BookSummary }>> {\n return call(ACCOUNTING_ACTIONS.createBook, input);\n}\n\nexport function updateBook(input: {\n bookId: string;\n name?: string;\n /** Pass `\"\"` to explicitly clear the country (server treats it as\n * the \"drop the field\" sentinel). Any other value must be one of\n * the curated `SupportedCountryCode`s. */\n country?: SupportedCountryCode | \"\";\n /** Q1..Q4 — pure metadata, only changes how the date-range\n * shortcuts resolve. No \"clear\" path; absence leaves the existing\n * value untouched. */\n fiscalYearEnd?: FiscalYearEnd;\n}): Promise<ApiResult<{ book: BookSummary }>> {\n return call(ACCOUNTING_ACTIONS.updateBook, input);\n}\n\nexport function deleteBook(bookId: string): Promise<ApiResult<{ deletedBookId: string; deletedBookName: string }>> {\n return call(ACCOUNTING_ACTIONS.deleteBook, { bookId, confirm: true });\n}\n\n// ── Accounts ─────────────────────────────────────────────────────────\n\nexport function getAccounts(bookId: string): Promise<ApiResult<{ bookId: string; accounts: Account[] }>> {\n return call(ACCOUNTING_ACTIONS.getAccounts, { bookId });\n}\n\nexport function upsertAccount(account: Account, bookId: string): Promise<ApiResult<{ bookId: string; account: Account; accounts: Account[] }>> {\n return call(ACCOUNTING_ACTIONS.upsertAccount, { account, bookId });\n}\n\n// ── Entries ──────────────────────────────────────────────────────────\n\nexport interface AddEntriesItemInput {\n date: string;\n lines: JournalLine[];\n memo?: string;\n /** When set, marks this entry as the replacement posted via the\n * \"edit\" flow. The caller is expected to have voided\n * `replacesEntryId` separately just before this call — there is\n * no atomic transaction. */\n replacesEntryId?: string;\n}\n\nexport function addEntries(input: {\n bookId: string;\n /** One or more entries to post. The server validates every entry\n * before any write, so a single bad entry rejects the whole\n * batch. Pass a single-element array to post just one entry. */\n entries: AddEntriesItemInput[];\n}): Promise<ApiResult<{ bookId: string; entries: JournalEntry[] }>> {\n return call(ACCOUNTING_ACTIONS.addEntries, input);\n}\n\nexport function voidEntry(input: {\n entryId: string;\n reason?: string;\n bookId: string;\n}): Promise<ApiResult<{ bookId: string; reverseEntry: JournalEntry; markerEntry: JournalEntry }>> {\n return call(ACCOUNTING_ACTIONS.voidEntry, input);\n}\n\nexport function getJournalEntries(input: {\n from?: string;\n to?: string;\n accountCode?: string;\n bookId: string;\n}): Promise<ApiResult<{ bookId: string; entries: JournalEntry[]; voidedEntryIds: string[] }>> {\n return call(ACCOUNTING_ACTIONS.getJournalEntries, input);\n}\n\n// ── Opening balances ─────────────────────────────────────────────────\n\nexport function getOpeningBalances(bookId: string): Promise<ApiResult<{ bookId: string; opening: JournalEntry | null }>> {\n return call(ACCOUNTING_ACTIONS.getOpeningBalances, { bookId });\n}\n\nexport function setOpeningBalances(input: {\n asOfDate: string;\n lines: JournalLine[];\n memo?: string;\n bookId: string;\n}): Promise<ApiResult<{ bookId: string; openingEntry: JournalEntry; replacedExisting: boolean }>> {\n return call(ACCOUNTING_ACTIONS.setOpeningBalances, input);\n}\n\n// ── Reports ──────────────────────────────────────────────────────────\n\nexport function getBalanceSheet(period: ReportPeriod, bookId: string): Promise<ApiResult<{ bookId: string; balanceSheet: BalanceSheet }>> {\n return call(ACCOUNTING_ACTIONS.getReport, { kind: \"balance\", period, bookId });\n}\n\nexport function getProfitLoss(period: ReportPeriod, bookId: string): Promise<ApiResult<{ bookId: string; profitLoss: ProfitLoss }>> {\n return call(ACCOUNTING_ACTIONS.getReport, { kind: \"pl\", period, bookId });\n}\n\nexport function getLedger(accountCode: string, period: ReportPeriod | undefined, bookId: string): Promise<ApiResult<{ bookId: string; ledger: Ledger }>> {\n return call(ACCOUNTING_ACTIONS.getReport, { kind: \"ledger\", accountCode, period, bookId });\n}\n\nexport interface TimeSeriesPoint {\n label: string;\n from: string;\n to: string;\n value: number;\n}\n\nexport interface TimeSeriesInput {\n bookId: string;\n metric: TimeSeriesMetric;\n granularity: TimeSeriesGranularity;\n /** Inclusive YYYY-MM-DD lower bound. The first bucket is the one\n * CONTAINING this date — it can extend earlier. */\n from: string;\n /** Inclusive YYYY-MM-DD upper bound. The last bucket is the one\n * CONTAINING this date — it can extend later. */\n to: string;\n /** Required when metric === \"accountBalance\"; forbidden otherwise.\n * The server returns a 400 either way. */\n accountCode?: string;\n}\n\nexport interface TimeSeriesResult {\n bookId: string;\n metric: TimeSeriesMetric;\n granularity: TimeSeriesGranularity;\n from: string;\n to: string;\n accountCode?: string;\n points: TimeSeriesPoint[];\n}\n\nexport function getTimeSeries(input: TimeSeriesInput): Promise<ApiResult<TimeSeriesResult>> {\n // Spread so the named interface is widened into a fresh object\n // literal — `call()` takes `Record<string, unknown>` which a\n // declared interface doesn't satisfy structurally in TS.\n return call(ACCOUNTING_ACTIONS.getTimeSeries, { ...input });\n}\n\n// ── Admin ────────────────────────────────────────────────────────────\n\nexport function rebuildSnapshots(bookId: string): Promise<ApiResult<{ bookId: string; rebuilt: string[] }>> {\n return call(ACCOUNTING_ACTIONS.rebuildSnapshots, { bookId });\n}\n","<template>\n <!-- Form for creating a new book. Two layouts share one body:\n • modal (default) — used by BookSwitcher's \"+ New book…\"\n sentinel option. Backdrop click cancels.\n • fullPage — used by View.vue on the first-run flow when\n the workspace has zero books. No backdrop, no cancel:\n the user MUST create their first book to proceed.\n The submit calls createBook directly; on success it emits\n the new book and its id, leaving the parent to update its\n current selection / refetch. -->\n <div :class=\"wrapperClass\" data-testid=\"accounting-new-book-modal\" @click.self=\"onBackdropClick\">\n <form class=\"bg-white p-4 rounded shadow-lg w-96 flex flex-col gap-3\" data-testid=\"accounting-new-book-form\" @submit.prevent=\"onSubmit\">\n <h3 class=\"text-base font-semibold\">{{ t(\"pluginAccounting.bookSwitcher.newBook\") }}</h3>\n <p v-if=\"firstRun\" class=\"text-xs text-gray-500\" data-testid=\"accounting-new-book-firstrun\">{{ t(\"pluginAccounting.bookSwitcher.firstRunHint\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.nameLabel\") }}\n <input ref=\"nameInput\" v-model=\"name\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-new-book-name\" />\n </label>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.currencyLabel\") }}\n <select v-model=\"currency\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-new-book-currency\">\n <option v-for=\"opt in options\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.countryLabel\") }}\n <select v-model=\"country\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-new-book-country\">\n <option value=\"\">{{ t(\"pluginAccounting.bookSwitcher.countryPlaceholder\") }}</option>\n <option v-for=\"opt in countryOptions\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.countryHint\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndLabel\") }}\n <select\n v-model=\"fiscalYearEnd\"\n required\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-new-book-fiscal-year-end\"\n >\n <option v-for=\"opt in fiscalYearEndOptions\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndHint\") }}</p>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-new-book-error\">{{ error }}</p>\n <div class=\"flex justify-end gap-2 mt-1\">\n <button v-if=\"showCancel\" type=\"button\" class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-700 hover:bg-gray-50\" @click=\"onCancel\">\n {{ t(\"pluginAccounting.common.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-2.5 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm\"\n :disabled=\"creating\"\n data-testid=\"accounting-new-book-submit\"\n >\n {{ creating ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.bookSwitcher.create\") }}\n </button>\n </div>\n </form>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, ref } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { createBook, type BookSummary } from \"../api\";\nimport {\n SUPPORTED_CURRENCY_CODES,\n localizedCurrencyName,\n SUPPORTED_COUNTRY_CODES,\n localizedCountryName,\n type SupportedCountryCode,\n DEFAULT_FISCAL_YEAR_END,\n FISCAL_YEAR_ENDS,\n type FiscalYearEnd,\n} from \"../../shared\";\n\nconst { t, locale } = useAccountingI18n();\n\nfunction regionFromLocaleTag(tag: string): SupportedCountryCode | \"\" {\n try {\n const { region } = new Intl.Locale(tag).maximize();\n if (region && (SUPPORTED_COUNTRY_CODES as readonly string[]).includes(region)) {\n return region as SupportedCountryCode;\n }\n } catch {\n /* fall through */\n }\n return \"\";\n}\n\nfunction guessDefaultCountry(uiLocaleTag: string): SupportedCountryCode | \"\" {\n // Try the active vue-i18n locale first (the user's *app* language\n // setting, e.g. \"ja-JP\"), then fall back to the browser's\n // navigator.language. Either may produce a region segment that maps\n // to a curated `SupportedCountryCode`. If neither does, leave the\n // field unset rather than silently picking a default — the\n // dropdown's \"(choose a country)\" option lets the user finish the\n // selection themselves so an unsupported locale doesn't quietly\n // become a US-jurisdiction book.\n const fromUi = regionFromLocaleTag(uiLocaleTag);\n if (fromUi !== \"\") return fromUi;\n const browserTag = typeof navigator !== \"undefined\" && typeof navigator.language === \"string\" ? navigator.language : \"\";\n return regionFromLocaleTag(browserTag);\n}\n\nconst props = withDefaults(\n defineProps<{\n firstRun?: boolean;\n cancelable?: boolean;\n fullPage?: boolean;\n }>(),\n { firstRun: false, cancelable: true, fullPage: false },\n);\n\nconst emit = defineEmits<{\n cancel: [];\n created: [book: BookSummary];\n}>();\n\nconst name = ref(\"\");\nconst currency = ref<string>(\"USD\");\nconst country = ref<SupportedCountryCode | \"\">(guessDefaultCountry(locale.value));\nconst fiscalYearEnd = ref<FiscalYearEnd>(DEFAULT_FISCAL_YEAR_END);\nconst creating = ref(false);\nconst error = ref<string | null>(null);\nconst nameInput = ref<HTMLInputElement | null>(null);\n\nonMounted(() => {\n // Land focus in Name on open — the only required field; the\n // currency select defaults to USD and the user usually leaves\n // it. Without this the user has to click into the field before\n // typing, which is friction for what should be a one-tap flow.\n void nextTick(() => nameInput.value?.focus());\n});\n\ninterface CurrencyOption {\n code: string;\n label: string;\n}\n\nconst options = computed<CurrencyOption[]>(() =>\n SUPPORTED_CURRENCY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCurrencyName(code, locale.value)}`,\n })),\n);\n\ninterface CountryOption {\n code: string;\n label: string;\n}\n\nconst countryOptions = computed<CountryOption[]>(() =>\n SUPPORTED_COUNTRY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCountryName(code, locale.value)}`,\n })),\n);\n\ninterface FiscalYearEndOption {\n value: FiscalYearEnd;\n label: string;\n}\n\nconst fiscalYearEndOptions = computed<FiscalYearEndOption[]>(() =>\n FISCAL_YEAR_ENDS.map((value) => ({\n value,\n label: t(`pluginAccounting.bookSwitcher.fiscalYearEnd${value}`),\n })),\n);\n\n// Full-page mode replaces the AccountingApp chrome — fill the\n// parent flex column with the form centered, no backdrop. Modal\n// mode keeps the original viewport overlay behaviour.\nconst wrapperClass = computed(() =>\n props.fullPage ? \"flex-1 bg-white flex items-center justify-center p-6 overflow-auto\" : \"fixed inset-0 z-50 bg-black/20 flex items-center justify-center\",\n);\n\n// Cancel is hidden in full-page mode regardless of `cancelable`\n// — the first-run flow forces the user to create a book.\nconst showCancel = computed(() => props.cancelable && !props.fullPage);\n\nfunction onBackdropClick(): void {\n if (props.fullPage) return;\n onCancel();\n}\n\nfunction onCancel(): void {\n if (!props.cancelable) return;\n emit(\"cancel\");\n}\n\nasync function onSubmit(): Promise<void> {\n if (creating.value) return;\n creating.value = true;\n error.value = null;\n try {\n // Only forward `country` when the user actually picked one — the\n // empty string is the dropdown's \"(choose a country)\" sentinel\n // and must not land on disk as a literal \"\" value.\n const pickedCountry: SupportedCountryCode | undefined = country.value === \"\" ? undefined : country.value;\n const result = await createBook({\n name: name.value.trim(),\n currency: currency.value,\n country: pickedCountry,\n fiscalYearEnd: fiscalYearEnd.value,\n });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n emit(\"created\", result.data.book);\n } finally {\n creating.value = false;\n }\n}\n</script>\n","<template>\n <!-- Form for creating a new book. Two layouts share one body:\n • modal (default) — used by BookSwitcher's \"+ New book…\"\n sentinel option. Backdrop click cancels.\n • fullPage — used by View.vue on the first-run flow when\n the workspace has zero books. No backdrop, no cancel:\n the user MUST create their first book to proceed.\n The submit calls createBook directly; on success it emits\n the new book and its id, leaving the parent to update its\n current selection / refetch. -->\n <div :class=\"wrapperClass\" data-testid=\"accounting-new-book-modal\" @click.self=\"onBackdropClick\">\n <form class=\"bg-white p-4 rounded shadow-lg w-96 flex flex-col gap-3\" data-testid=\"accounting-new-book-form\" @submit.prevent=\"onSubmit\">\n <h3 class=\"text-base font-semibold\">{{ t(\"pluginAccounting.bookSwitcher.newBook\") }}</h3>\n <p v-if=\"firstRun\" class=\"text-xs text-gray-500\" data-testid=\"accounting-new-book-firstrun\">{{ t(\"pluginAccounting.bookSwitcher.firstRunHint\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.nameLabel\") }}\n <input ref=\"nameInput\" v-model=\"name\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-new-book-name\" />\n </label>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.currencyLabel\") }}\n <select v-model=\"currency\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-new-book-currency\">\n <option v-for=\"opt in options\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.countryLabel\") }}\n <select v-model=\"country\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-new-book-country\">\n <option value=\"\">{{ t(\"pluginAccounting.bookSwitcher.countryPlaceholder\") }}</option>\n <option v-for=\"opt in countryOptions\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.countryHint\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndLabel\") }}\n <select\n v-model=\"fiscalYearEnd\"\n required\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-new-book-fiscal-year-end\"\n >\n <option v-for=\"opt in fiscalYearEndOptions\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndHint\") }}</p>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-new-book-error\">{{ error }}</p>\n <div class=\"flex justify-end gap-2 mt-1\">\n <button v-if=\"showCancel\" type=\"button\" class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-700 hover:bg-gray-50\" @click=\"onCancel\">\n {{ t(\"pluginAccounting.common.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-2.5 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm\"\n :disabled=\"creating\"\n data-testid=\"accounting-new-book-submit\"\n >\n {{ creating ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.bookSwitcher.create\") }}\n </button>\n </div>\n </form>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, ref } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { createBook, type BookSummary } from \"../api\";\nimport {\n SUPPORTED_CURRENCY_CODES,\n localizedCurrencyName,\n SUPPORTED_COUNTRY_CODES,\n localizedCountryName,\n type SupportedCountryCode,\n DEFAULT_FISCAL_YEAR_END,\n FISCAL_YEAR_ENDS,\n type FiscalYearEnd,\n} from \"../../shared\";\n\nconst { t, locale } = useAccountingI18n();\n\nfunction regionFromLocaleTag(tag: string): SupportedCountryCode | \"\" {\n try {\n const { region } = new Intl.Locale(tag).maximize();\n if (region && (SUPPORTED_COUNTRY_CODES as readonly string[]).includes(region)) {\n return region as SupportedCountryCode;\n }\n } catch {\n /* fall through */\n }\n return \"\";\n}\n\nfunction guessDefaultCountry(uiLocaleTag: string): SupportedCountryCode | \"\" {\n // Try the active vue-i18n locale first (the user's *app* language\n // setting, e.g. \"ja-JP\"), then fall back to the browser's\n // navigator.language. Either may produce a region segment that maps\n // to a curated `SupportedCountryCode`. If neither does, leave the\n // field unset rather than silently picking a default — the\n // dropdown's \"(choose a country)\" option lets the user finish the\n // selection themselves so an unsupported locale doesn't quietly\n // become a US-jurisdiction book.\n const fromUi = regionFromLocaleTag(uiLocaleTag);\n if (fromUi !== \"\") return fromUi;\n const browserTag = typeof navigator !== \"undefined\" && typeof navigator.language === \"string\" ? navigator.language : \"\";\n return regionFromLocaleTag(browserTag);\n}\n\nconst props = withDefaults(\n defineProps<{\n firstRun?: boolean;\n cancelable?: boolean;\n fullPage?: boolean;\n }>(),\n { firstRun: false, cancelable: true, fullPage: false },\n);\n\nconst emit = defineEmits<{\n cancel: [];\n created: [book: BookSummary];\n}>();\n\nconst name = ref(\"\");\nconst currency = ref<string>(\"USD\");\nconst country = ref<SupportedCountryCode | \"\">(guessDefaultCountry(locale.value));\nconst fiscalYearEnd = ref<FiscalYearEnd>(DEFAULT_FISCAL_YEAR_END);\nconst creating = ref(false);\nconst error = ref<string | null>(null);\nconst nameInput = ref<HTMLInputElement | null>(null);\n\nonMounted(() => {\n // Land focus in Name on open — the only required field; the\n // currency select defaults to USD and the user usually leaves\n // it. Without this the user has to click into the field before\n // typing, which is friction for what should be a one-tap flow.\n void nextTick(() => nameInput.value?.focus());\n});\n\ninterface CurrencyOption {\n code: string;\n label: string;\n}\n\nconst options = computed<CurrencyOption[]>(() =>\n SUPPORTED_CURRENCY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCurrencyName(code, locale.value)}`,\n })),\n);\n\ninterface CountryOption {\n code: string;\n label: string;\n}\n\nconst countryOptions = computed<CountryOption[]>(() =>\n SUPPORTED_COUNTRY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCountryName(code, locale.value)}`,\n })),\n);\n\ninterface FiscalYearEndOption {\n value: FiscalYearEnd;\n label: string;\n}\n\nconst fiscalYearEndOptions = computed<FiscalYearEndOption[]>(() =>\n FISCAL_YEAR_ENDS.map((value) => ({\n value,\n label: t(`pluginAccounting.bookSwitcher.fiscalYearEnd${value}`),\n })),\n);\n\n// Full-page mode replaces the AccountingApp chrome — fill the\n// parent flex column with the form centered, no backdrop. Modal\n// mode keeps the original viewport overlay behaviour.\nconst wrapperClass = computed(() =>\n props.fullPage ? \"flex-1 bg-white flex items-center justify-center p-6 overflow-auto\" : \"fixed inset-0 z-50 bg-black/20 flex items-center justify-center\",\n);\n\n// Cancel is hidden in full-page mode regardless of `cancelable`\n// — the first-run flow forces the user to create a book.\nconst showCancel = computed(() => props.cancelable && !props.fullPage);\n\nfunction onBackdropClick(): void {\n if (props.fullPage) return;\n onCancel();\n}\n\nfunction onCancel(): void {\n if (!props.cancelable) return;\n emit(\"cancel\");\n}\n\nasync function onSubmit(): Promise<void> {\n if (creating.value) return;\n creating.value = true;\n error.value = null;\n try {\n // Only forward `country` when the user actually picked one — the\n // empty string is the dropdown's \"(choose a country)\" sentinel\n // and must not land on disk as a literal \"\" value.\n const pickedCountry: SupportedCountryCode | undefined = country.value === \"\" ? undefined : country.value;\n const result = await createBook({\n name: name.value.trim(),\n currency: currency.value,\n country: pickedCountry,\n fiscalYearEnd: fiscalYearEnd.value,\n });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n emit(\"created\", result.data.book);\n } finally {\n creating.value = false;\n }\n}\n</script>\n","<template>\n <div class=\"flex items-center gap-2\">\n <label class=\"text-xs text-gray-500\" for=\"accounting-book-select\">{{ t(\"pluginAccounting.bookSwitcher.label\") }}</label>\n <select\n id=\"accounting-book-select\"\n :value=\"modelValue\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-book-select\"\n @change=\"onSelect\"\n >\n <option v-if=\"modelValue === ''\" value=\"\" disabled>{{ t(\"pluginAccounting.bookSwitcher.placeholder\") }}</option>\n <option v-for=\"book in books\" :key=\"book.id\" :value=\"book.id\">{{ formatBookOption(book) }}</option>\n <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -- decorative separator inside the books <select>, not user copy -->\n <option disabled>──────────</option>\n <option :value=\"NEW_BOOK_SENTINEL\" data-testid=\"accounting-new-book-option\">+ {{ t(\"pluginAccounting.bookSwitcher.newBook\") }}</option>\n </select>\n <NewBookForm v-if=\"showNewBook\" @cancel=\"showNewBook = false\" @created=\"onCreated\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport NewBookForm from \"./NewBookForm.vue\";\nimport type { BookSummary } from \"../api\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ modelValue: string; books: BookSummary[] }>();\nconst emit = defineEmits<{\n \"update:modelValue\": [bookId: string];\n \"books-changed\": [];\n \"book-created\": [book: BookSummary];\n}>();\n\n// Sentinel value for the \"+ New book\" option living inside the\n// books <select>. Picking it opens the modal and reverts the\n// select's displayed value to the current selection — the option\n// must not collide with any real book id, which are nanoid-shaped.\nconst NEW_BOOK_SENTINEL = \"__new__\";\n\nconst showNewBook = ref(false);\n\nfunction formatBookOption(book: BookSummary): string {\n // `Name (CCY · Country)` when a country is set; otherwise fall back\n // to `Name (CCY)`. Keeps the option label compact while surfacing\n // the country so a multi-jurisdiction user can pick the right book\n // by tax regime, not just currency.\n const suffix = book.country ? `${book.currency} · ${book.country}` : book.currency;\n return `${book.name} (${suffix})`;\n}\n\nfunction onSelect(event: Event): void {\n const target = event.target as HTMLSelectElement;\n const bookId = target.value;\n if (bookId === NEW_BOOK_SENTINEL) {\n target.value = props.modelValue;\n showNewBook.value = true;\n return;\n }\n if (bookId === props.modelValue) return;\n // The View persists the new selection to localStorage; no server\n // round-trip needed since there's no shared \"active book\" state.\n emit(\"update:modelValue\", bookId);\n}\n\nfunction onCreated(book: BookSummary): void {\n // Hand the new book to the parent in one event so it can await\n // its own refetch before setting the active selection. Splitting\n // this into separate `books-changed` + `update:modelValue` emits\n // races: the parent's async refetch runs concurrently with the\n // selection update, and the stillExists guard inside refetch can\n // snap the selection back to books[0] if the fetch happens to\n // resolve before the new book is in the list.\n showNewBook.value = false;\n emit(\"book-created\", book);\n}\n</script>\n","<template>\n <div class=\"flex items-center gap-2\">\n <label class=\"text-xs text-gray-500\" for=\"accounting-book-select\">{{ t(\"pluginAccounting.bookSwitcher.label\") }}</label>\n <select\n id=\"accounting-book-select\"\n :value=\"modelValue\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-book-select\"\n @change=\"onSelect\"\n >\n <option v-if=\"modelValue === ''\" value=\"\" disabled>{{ t(\"pluginAccounting.bookSwitcher.placeholder\") }}</option>\n <option v-for=\"book in books\" :key=\"book.id\" :value=\"book.id\">{{ formatBookOption(book) }}</option>\n <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -- decorative separator inside the books <select>, not user copy -->\n <option disabled>──────────</option>\n <option :value=\"NEW_BOOK_SENTINEL\" data-testid=\"accounting-new-book-option\">+ {{ t(\"pluginAccounting.bookSwitcher.newBook\") }}</option>\n </select>\n <NewBookForm v-if=\"showNewBook\" @cancel=\"showNewBook = false\" @created=\"onCreated\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport NewBookForm from \"./NewBookForm.vue\";\nimport type { BookSummary } from \"../api\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ modelValue: string; books: BookSummary[] }>();\nconst emit = defineEmits<{\n \"update:modelValue\": [bookId: string];\n \"books-changed\": [];\n \"book-created\": [book: BookSummary];\n}>();\n\n// Sentinel value for the \"+ New book\" option living inside the\n// books <select>. Picking it opens the modal and reverts the\n// select's displayed value to the current selection — the option\n// must not collide with any real book id, which are nanoid-shaped.\nconst NEW_BOOK_SENTINEL = \"__new__\";\n\nconst showNewBook = ref(false);\n\nfunction formatBookOption(book: BookSummary): string {\n // `Name (CCY · Country)` when a country is set; otherwise fall back\n // to `Name (CCY)`. Keeps the option label compact while surfacing\n // the country so a multi-jurisdiction user can pick the right book\n // by tax regime, not just currency.\n const suffix = book.country ? `${book.currency} · ${book.country}` : book.currency;\n return `${book.name} (${suffix})`;\n}\n\nfunction onSelect(event: Event): void {\n const target = event.target as HTMLSelectElement;\n const bookId = target.value;\n if (bookId === NEW_BOOK_SENTINEL) {\n target.value = props.modelValue;\n showNewBook.value = true;\n return;\n }\n if (bookId === props.modelValue) return;\n // The View persists the new selection to localStorage; no server\n // round-trip needed since there's no shared \"active book\" state.\n emit(\"update:modelValue\", bookId);\n}\n\nfunction onCreated(book: BookSummary): void {\n // Hand the new book to the parent in one event so it can await\n // its own refetch before setting the active selection. Splitting\n // this into separate `books-changed` + `update:modelValue` emits\n // races: the parent's async refetch runs concurrently with the\n // selection update, and the stillExists guard inside refetch can\n // snap the selection back to books[0] if the fetch happens to\n // resolve before the new book is in the list.\n showNewBook.value = false;\n emit(\"book-created\", book);\n}\n</script>\n","// Stale-response guard for watcher-driven async fetches.\n//\n// Pattern: a watcher fires on bookId / filter / version changes\n// and kicks off `apiPost(...)`. Without coordination, a slower\n// earlier request can resolve after a newer one and overwrite the\n// fresh state with stale data. This composable hands out a\n// monotonic token before each await; the caller checks that the\n// token is still current after the await before mutating state.\n//\n// Usage:\n//\n// const { begin, isCurrent } = useLatestRequest();\n// async function refresh() {\n// const token = begin();\n// const result = await api.fetch(...);\n// if (!isCurrent(token)) return; // a newer refresh started\n// applyState(result);\n// }\n//\n// Cheap and dependency-free. Each component holds its own\n// `useLatestRequest()` instance — there's no shared state across\n// components.\n\nexport interface LatestRequestApi {\n /** Returns the token of the new request. Increments the\n * internal counter; older outstanding requests will fail\n * `isCurrent`. */\n begin: () => number;\n /** True if `token` is still the most recently issued one. */\n isCurrent: (token: number) => boolean;\n}\n\nexport function useLatestRequest(): LatestRequestApi {\n let counter = 0;\n return {\n begin(): number {\n counter += 1;\n return counter;\n },\n isCurrent(token: number): boolean {\n return token === counter;\n },\n };\n}\n","<template>\n <!-- Reusable from/to + shortcut date range picker. Owns no state\n beyond the v-model; the parent supplies an initial range and\n the active book's fiscalYearEnd so quarter/year shortcuts\n resolve under the right calendar. -->\n <div class=\"flex flex-wrap items-end gap-2\" data-testid=\"accounting-daterange\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.shortcutLabel\") }}\n <select\n :value=\"selectedShortcut\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-daterange-shortcut\"\n @change=\"onShortcutChange(($event.target as HTMLSelectElement).value)\"\n >\n <!-- Sentinel for the \"custom\" state. Hidden from the menu\n but bound when the current range doesn't match any\n preset, which leaves the trigger displaying blank\n instead of forcing a wrong-looking match. -->\n <option value=\"\" hidden></option>\n <option value=\"currentQuarter\">{{ t(\"pluginAccounting.dateRange.currentQuarter\") }}</option>\n <option value=\"previousQuarter\">{{ t(\"pluginAccounting.dateRange.previousQuarter\") }}</option>\n <option value=\"currentYear\">{{ t(\"pluginAccounting.dateRange.currentYear\") }}</option>\n <option value=\"previousYear\">{{ t(\"pluginAccounting.dateRange.previousYear\") }}</option>\n <option v-if=\"hasOpeningDate\" value=\"lifetime\">{{ t(\"pluginAccounting.dateRange.lifetime\") }}</option>\n <option value=\"all\">{{ t(\"pluginAccounting.dateRange.all\") }}</option>\n </select>\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.fromLabel\") }}\n <input\n :value=\"modelValue.from\"\n type=\"date\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm\"\n data-testid=\"accounting-daterange-from\"\n @input=\"onFromChange(($event.target as HTMLInputElement).value)\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.toLabel\") }}\n <input\n :value=\"modelValue.to\"\n type=\"date\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm\"\n data-testid=\"accounting-daterange-to\"\n @input=\"onToChange(($event.target as HTMLInputElement).value)\"\n />\n </label>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport {\n currentFiscalYearRange,\n currentQuarterRange,\n previousFiscalYearRange,\n previousQuarterRange,\n type DateRange,\n type FiscalYearEnd,\n localDateString,\n} from \"../../shared\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{\n modelValue: DateRange;\n fiscalYearEnd: FiscalYearEnd;\n /** The active book's opening-balance date. Drives the \"Lifetime\"\n * shortcut (from = openingDate, to = today). Optional — when\n * absent the Lifetime option is hidden from the menu. The opening\n * gate prevents the tabs that mount this picker from rendering\n * before an opening exists, so in normal use this stays defined. */\n openingDate?: string;\n}>();\n\nconst emit = defineEmits<{\n \"update:modelValue\": [DateRange];\n}>();\n\nconst hasOpeningDate = computed<boolean>(() => Boolean(props.openingDate));\n\nconst UNBOUNDED_RANGE: DateRange = { from: \"\", to: \"\" };\n\n/** From the book's opening date through today. Hidden from the menu\n * when the parent hasn't supplied an opening. */\nfunction lifetimeRange(): DateRange | null {\n if (!props.openingDate) return null;\n return { from: props.openingDate, to: localDateString() };\n}\n\ntype Shortcut = \"currentQuarter\" | \"previousQuarter\" | \"currentYear\" | \"previousYear\" | \"lifetime\" | \"all\";\n/** Empty string is the sentinel \"no preset matches\" value bound to\n * the hidden option in the template — the trigger shows blank. */\ntype SelectedShortcut = Shortcut | \"\";\n\nfunction rangesEqual(left: DateRange, right: DateRange): boolean {\n return left.from === right.from && left.to === right.to;\n}\n\n// Resolve the dropdown's displayed value from the current\n// modelValue. Re-evaluates today on every read — the picker is a\n// short-lived UI surface so cache invalidation isn't a concern, and\n// the user has no expectation that \"current quarter\" picked in the\n// morning still labels correctly at midnight. Returns \"\" when no\n// preset matches (custom range from manual from/to edits).\n//\n// Order matters when ranges collide: when no opening is on file the\n// Lifetime option is hidden from the menu, but if it ever produced\n// the same span as another preset (it can't — Lifetime spans years,\n// presets span quarter/year), the earlier branch would win. We\n// check the explicit ranges first and fall through to the unbounded\n// \"all\" last so a manually-cleared input lands on \"all\" rather than\n// blank when both sides happen to be empty.\nconst selectedShortcut = computed<SelectedShortcut>(() => {\n const value = props.modelValue;\n const today = new Date();\n if (rangesEqual(value, currentQuarterRange(props.fiscalYearEnd, today))) return \"currentQuarter\";\n if (rangesEqual(value, previousQuarterRange(props.fiscalYearEnd, today))) return \"previousQuarter\";\n if (rangesEqual(value, currentFiscalYearRange(props.fiscalYearEnd, today))) return \"currentYear\";\n if (rangesEqual(value, previousFiscalYearRange(props.fiscalYearEnd, today))) return \"previousYear\";\n const lifetime = lifetimeRange();\n if (lifetime && rangesEqual(value, lifetime)) return \"lifetime\";\n if (rangesEqual(value, UNBOUNDED_RANGE)) return \"all\";\n return \"\";\n});\n\nfunction onShortcutChange(raw: string): void {\n const today = new Date();\n if (raw === \"currentQuarter\") emit(\"update:modelValue\", currentQuarterRange(props.fiscalYearEnd, today));\n else if (raw === \"previousQuarter\") emit(\"update:modelValue\", previousQuarterRange(props.fiscalYearEnd, today));\n else if (raw === \"currentYear\") emit(\"update:modelValue\", currentFiscalYearRange(props.fiscalYearEnd, today));\n else if (raw === \"previousYear\") emit(\"update:modelValue\", previousFiscalYearRange(props.fiscalYearEnd, today));\n else if (raw === \"lifetime\") {\n const lifetime = lifetimeRange();\n if (lifetime) emit(\"update:modelValue\", lifetime);\n } else if (raw === \"all\") emit(\"update:modelValue\", UNBOUNDED_RANGE);\n}\n\nfunction onFromChange(value: string): void {\n emit(\"update:modelValue\", { from: value, to: props.modelValue.to });\n}\n\nfunction onToChange(value: string): void {\n emit(\"update:modelValue\", { from: props.modelValue.from, to: value });\n}\n</script>\n","<template>\n <!-- Reusable from/to + shortcut date range picker. Owns no state\n beyond the v-model; the parent supplies an initial range and\n the active book's fiscalYearEnd so quarter/year shortcuts\n resolve under the right calendar. -->\n <div class=\"flex flex-wrap items-end gap-2\" data-testid=\"accounting-daterange\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.shortcutLabel\") }}\n <select\n :value=\"selectedShortcut\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-daterange-shortcut\"\n @change=\"onShortcutChange(($event.target as HTMLSelectElement).value)\"\n >\n <!-- Sentinel for the \"custom\" state. Hidden from the menu\n but bound when the current range doesn't match any\n preset, which leaves the trigger displaying blank\n instead of forcing a wrong-looking match. -->\n <option value=\"\" hidden></option>\n <option value=\"currentQuarter\">{{ t(\"pluginAccounting.dateRange.currentQuarter\") }}</option>\n <option value=\"previousQuarter\">{{ t(\"pluginAccounting.dateRange.previousQuarter\") }}</option>\n <option value=\"currentYear\">{{ t(\"pluginAccounting.dateRange.currentYear\") }}</option>\n <option value=\"previousYear\">{{ t(\"pluginAccounting.dateRange.previousYear\") }}</option>\n <option v-if=\"hasOpeningDate\" value=\"lifetime\">{{ t(\"pluginAccounting.dateRange.lifetime\") }}</option>\n <option value=\"all\">{{ t(\"pluginAccounting.dateRange.all\") }}</option>\n </select>\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.fromLabel\") }}\n <input\n :value=\"modelValue.from\"\n type=\"date\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm\"\n data-testid=\"accounting-daterange-from\"\n @input=\"onFromChange(($event.target as HTMLInputElement).value)\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.dateRange.toLabel\") }}\n <input\n :value=\"modelValue.to\"\n type=\"date\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm\"\n data-testid=\"accounting-daterange-to\"\n @input=\"onToChange(($event.target as HTMLInputElement).value)\"\n />\n </label>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport {\n currentFiscalYearRange,\n currentQuarterRange,\n previousFiscalYearRange,\n previousQuarterRange,\n type DateRange,\n type FiscalYearEnd,\n localDateString,\n} from \"../../shared\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{\n modelValue: DateRange;\n fiscalYearEnd: FiscalYearEnd;\n /** The active book's opening-balance date. Drives the \"Lifetime\"\n * shortcut (from = openingDate, to = today). Optional — when\n * absent the Lifetime option is hidden from the menu. The opening\n * gate prevents the tabs that mount this picker from rendering\n * before an opening exists, so in normal use this stays defined. */\n openingDate?: string;\n}>();\n\nconst emit = defineEmits<{\n \"update:modelValue\": [DateRange];\n}>();\n\nconst hasOpeningDate = computed<boolean>(() => Boolean(props.openingDate));\n\nconst UNBOUNDED_RANGE: DateRange = { from: \"\", to: \"\" };\n\n/** From the book's opening date through today. Hidden from the menu\n * when the parent hasn't supplied an opening. */\nfunction lifetimeRange(): DateRange | null {\n if (!props.openingDate) return null;\n return { from: props.openingDate, to: localDateString() };\n}\n\ntype Shortcut = \"currentQuarter\" | \"previousQuarter\" | \"currentYear\" | \"previousYear\" | \"lifetime\" | \"all\";\n/** Empty string is the sentinel \"no preset matches\" value bound to\n * the hidden option in the template — the trigger shows blank. */\ntype SelectedShortcut = Shortcut | \"\";\n\nfunction rangesEqual(left: DateRange, right: DateRange): boolean {\n return left.from === right.from && left.to === right.to;\n}\n\n// Resolve the dropdown's displayed value from the current\n// modelValue. Re-evaluates today on every read — the picker is a\n// short-lived UI surface so cache invalidation isn't a concern, and\n// the user has no expectation that \"current quarter\" picked in the\n// morning still labels correctly at midnight. Returns \"\" when no\n// preset matches (custom range from manual from/to edits).\n//\n// Order matters when ranges collide: when no opening is on file the\n// Lifetime option is hidden from the menu, but if it ever produced\n// the same span as another preset (it can't — Lifetime spans years,\n// presets span quarter/year), the earlier branch would win. We\n// check the explicit ranges first and fall through to the unbounded\n// \"all\" last so a manually-cleared input lands on \"all\" rather than\n// blank when both sides happen to be empty.\nconst selectedShortcut = computed<SelectedShortcut>(() => {\n const value = props.modelValue;\n const today = new Date();\n if (rangesEqual(value, currentQuarterRange(props.fiscalYearEnd, today))) return \"currentQuarter\";\n if (rangesEqual(value, previousQuarterRange(props.fiscalYearEnd, today))) return \"previousQuarter\";\n if (rangesEqual(value, currentFiscalYearRange(props.fiscalYearEnd, today))) return \"currentYear\";\n if (rangesEqual(value, previousFiscalYearRange(props.fiscalYearEnd, today))) return \"previousYear\";\n const lifetime = lifetimeRange();\n if (lifetime && rangesEqual(value, lifetime)) return \"lifetime\";\n if (rangesEqual(value, UNBOUNDED_RANGE)) return \"all\";\n return \"\";\n});\n\nfunction onShortcutChange(raw: string): void {\n const today = new Date();\n if (raw === \"currentQuarter\") emit(\"update:modelValue\", currentQuarterRange(props.fiscalYearEnd, today));\n else if (raw === \"previousQuarter\") emit(\"update:modelValue\", previousQuarterRange(props.fiscalYearEnd, today));\n else if (raw === \"currentYear\") emit(\"update:modelValue\", currentFiscalYearRange(props.fiscalYearEnd, today));\n else if (raw === \"previousYear\") emit(\"update:modelValue\", previousFiscalYearRange(props.fiscalYearEnd, today));\n else if (raw === \"lifetime\") {\n const lifetime = lifetimeRange();\n if (lifetime) emit(\"update:modelValue\", lifetime);\n } else if (raw === \"all\") emit(\"update:modelValue\", UNBOUNDED_RANGE);\n}\n\nfunction onFromChange(value: string): void {\n emit(\"update:modelValue\", { from: value, to: props.modelValue.to });\n}\n\nfunction onToChange(value: string): void {\n emit(\"update:modelValue\", { from: props.modelValue.from, to: value });\n}\n</script>\n","// Account-code numbering convention. The chart of accounts uses\n// 4-digit codes whose leading digit identifies the type:\n//\n// 1xxx → asset\n// 2xxx → liability\n// 3xxx → equity\n// 4xxx → income\n// 5xxx → expense\n//\n// Within those bands, the second digit `4` is reserved for tax-\n// related accounts on both sides of the balance sheet:\n//\n// 14xx → tax-related current assets\n// (1400 Input Tax Receivable / 仮払消費税, plus future\n// withholding-tax-receivable / etc. siblings)\n// 24xx → tax-related current liabilities\n// (2400 Sales Tax Payable / 仮受消費税, plus future\n// withholding-tax-payable / etc. siblings)\n//\n// Special-case UI (Ledger T-number column, JournalEntryForm\n// per-line tax-registration ID input) is **input-tax-only** — it\n// keys off `isTaxAccountCode`, which matches 14xx (purchase side)\n// only. Output-tax / sales-side lines (24xx) intentionally don't\n// surface a counterparty registration field: the seller's\n// obligation is to put their *own* registration number on the\n// invoice they issue, not to capture the customer's. So a custom\n// suspense account added in the 14xx band participates without\n// any opt-in step; 24xx accounts book the liability without the\n// extra column.\n//\n// Lives in its own module so AccountsModal, AccountEditor, and the\n// validation helper can share the same constants without circular\n// imports between Vue components.\n\nimport type { Account, AccountType } from \"../api\";\n\nexport const ACCOUNT_TYPE_PREFIX: Record<AccountType, number> = {\n asset: 1,\n liability: 2,\n equity: 3,\n income: 4,\n expense: 5,\n};\n\nconst TAX_ACCOUNT_PREFIXES: readonly string[] = [\"14\"];\n\n/** Returns `true` for codes whose first two digits identify a\n * tax-related current asset (`14xx`) — i.e. the input-tax /\n * purchase side of consumption / sales / VAT bookkeeping. Drives\n * Ledger column visibility and the JournalEntryForm per-line\n * tax-registration ID input. Output-tax (24xx) is intentionally\n * excluded: the counterparty's registration ID is only\n * load-bearing for input-tax-credit eligibility on purchases. */\nexport function isTaxAccountCode(code: string): boolean {\n return TAX_ACCOUNT_PREFIXES.some((prefix) => code.startsWith(prefix));\n}\n\nconst ACCOUNT_CODE_RE = /^\\d{4}$/;\nconst SUGGESTED_GAP = 10;\n\nexport function isValidAccountCode(code: string): boolean {\n return ACCOUNT_CODE_RE.test(code);\n}\n\nexport function typeForCode(code: string): AccountType | null {\n if (!isValidAccountCode(code)) return null;\n const leading = Number.parseInt(code[0], 10);\n for (const [type, prefix] of Object.entries(ACCOUNT_TYPE_PREFIX) as [AccountType, number][]) {\n if (prefix === leading) return type;\n }\n return null;\n}\n\nexport function codeMatchesType(code: string, type: AccountType): boolean {\n return typeForCode(code) === type;\n}\n\n/** Suggest the next free 4-digit code for `type`. Picks max-in-range\n * + SUGGESTED_GAP so users keep room to insert sibling accounts\n * later (the standard accounting convention). Falls back to the\n * prefix base when the range is empty, and to max+1 when +gap would\n * spill out of the 4-digit prefix window. */\nexport function suggestNextCode(type: AccountType, accounts: readonly Account[]): string {\n const prefix = ACCOUNT_TYPE_PREFIX[type];\n const inRange: number[] = [];\n for (const account of accounts) {\n if (!isValidAccountCode(account.code)) continue;\n const value = Number.parseInt(account.code, 10);\n if (Math.floor(value / 1000) !== prefix) continue;\n inRange.push(value);\n }\n if (inRange.length === 0) return `${prefix}000`;\n const max = Math.max(...inRange);\n const candidate = max + SUGGESTED_GAP;\n if (Math.floor(candidate / 1000) === prefix && candidate <= 9999) return String(candidate);\n // Range is dense at the top — fall back to a unit step. If even\n // that overflows the prefix window the chart is essentially full\n // for that type; surface the overflow rather than silently\n // suggesting a code in the next type's range.\n const fallback = max + 1;\n if (Math.floor(fallback / 1000) === prefix && fallback <= 9999) return String(fallback);\n return `${prefix}999`;\n}\n","<template>\n <!-- One row in the AccountsModal list. Read-only display + an\n active checkbox (left column) and an Edit button (right) for\n active rows. The editor itself is AccountEditor.vue, mounted\n in place of this row by the parent when editing. -->\n <div :class=\"['flex items-center gap-2 px-2 py-0.5 text-sm', inactive ? 'opacity-60' : '']\" :data-testid=\"`accounting-accounts-row-${account.code}`\">\n <input\n type=\"checkbox\"\n :checked=\"!inactive\"\n :title=\"inactive ? t('pluginAccounting.accounts.reactivate') : t('pluginAccounting.accounts.deactivate')\"\n :aria-label=\"inactive ? t('pluginAccounting.accounts.reactivate') : t('pluginAccounting.accounts.deactivate')\"\n class=\"h-4 w-4 shrink-0 cursor-pointer\"\n :data-testid=\"`accounting-accounts-toggle-${account.code}`\"\n @change=\"emit('toggleActive')\"\n />\n <span class=\"font-mono text-xs text-gray-500 w-16 shrink-0\">{{ account.code }}</span>\n <span\n :class=\"['grow min-w-0 truncate', inactive ? 'line-through' : '']\"\n :data-testid=\"inactive ? `accounting-accounts-inactive-${account.code}` : undefined\"\n >{{ account.name }}</span\n >\n <span v-if=\"account.note\" class=\"text-xs text-gray-400 truncate max-w-[8rem]\" :title=\"account.note\">{{ account.note }}</span>\n <!-- Always rendered (with `invisible` when inactive) so checking\n and unchecking the active box doesn't shift the row width. -->\n <button\n type=\"button\"\n :class=\"['h-8 px-2.5 rounded text-sm text-blue-600 hover:bg-blue-50', inactive ? 'invisible' : '']\"\n :data-testid=\"`accounting-accounts-edit-${account.code}`\"\n :disabled=\"inactive\"\n :aria-hidden=\"inactive ? 'true' : undefined\"\n :tabindex=\"inactive ? -1 : undefined\"\n @click=\"emit('edit')\"\n >\n {{ t(\"pluginAccounting.accounts.edit\") }}\n </button>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport type { Account } from \"../api\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ account: Account }>();\nconst emit = defineEmits<{ edit: []; toggleActive: [] }>();\n\nconst inactive = computed(() => props.account.active === false);\n</script>\n","<template>\n <!-- One row in the AccountsModal list. Read-only display + an\n active checkbox (left column) and an Edit button (right) for\n active rows. The editor itself is AccountEditor.vue, mounted\n in place of this row by the parent when editing. -->\n <div :class=\"['flex items-center gap-2 px-2 py-0.5 text-sm', inactive ? 'opacity-60' : '']\" :data-testid=\"`accounting-accounts-row-${account.code}`\">\n <input\n type=\"checkbox\"\n :checked=\"!inactive\"\n :title=\"inactive ? t('pluginAccounting.accounts.reactivate') : t('pluginAccounting.accounts.deactivate')\"\n :aria-label=\"inactive ? t('pluginAccounting.accounts.reactivate') : t('pluginAccounting.accounts.deactivate')\"\n class=\"h-4 w-4 shrink-0 cursor-pointer\"\n :data-testid=\"`accounting-accounts-toggle-${account.code}`\"\n @change=\"emit('toggleActive')\"\n />\n <span class=\"font-mono text-xs text-gray-500 w-16 shrink-0\">{{ account.code }}</span>\n <span\n :class=\"['grow min-w-0 truncate', inactive ? 'line-through' : '']\"\n :data-testid=\"inactive ? `accounting-accounts-inactive-${account.code}` : undefined\"\n >{{ account.name }}</span\n >\n <span v-if=\"account.note\" class=\"text-xs text-gray-400 truncate max-w-[8rem]\" :title=\"account.note\">{{ account.note }}</span>\n <!-- Always rendered (with `invisible` when inactive) so checking\n and unchecking the active box doesn't shift the row width. -->\n <button\n type=\"button\"\n :class=\"['h-8 px-2.5 rounded text-sm text-blue-600 hover:bg-blue-50', inactive ? 'invisible' : '']\"\n :data-testid=\"`accounting-accounts-edit-${account.code}`\"\n :disabled=\"inactive\"\n :aria-hidden=\"inactive ? 'true' : undefined\"\n :tabindex=\"inactive ? -1 : undefined\"\n @click=\"emit('edit')\"\n >\n {{ t(\"pluginAccounting.accounts.edit\") }}\n </button>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport type { Account } from \"../api\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ account: Account }>();\nconst emit = defineEmits<{ edit: []; toggleActive: [] }>();\n\nconst inactive = computed(() => props.account.active === false);\n</script>\n","// Pure validation for the AccountsModal editor draft. Lives in its\n// own module so unit tests can exercise the boundary cases (reserved\n// `_` prefix, duplicate code, empty fields) without spinning up Vue\n// or i18n. The component maps the returned error code to a\n// localized message.\n//\n// The `_`-prefix rule mirrors the server's check in\n// server/accounting/service.ts:upsertAccount — codes starting with\n// `_` are reserved for synthetic report rows. Catching it client-\n// side avoids a round-trip and surfaces the localized message\n// instead of the raw server error.\n\nimport type { Account } from \"../api\";\nimport type { AccountDraft } from \"./accountDraft\";\nimport { codeMatchesType, isValidAccountCode } from \"./accountNumbering\";\n\nexport const RESERVED_PREFIX = \"_\";\n\nexport type CodeValidationError = \"emptyCode\" | \"reservedCode\" | \"invalidCodeFormat\" | \"codeTypeMismatch\" | \"duplicateCode\";\nexport type NameValidationError = \"emptyName\" | \"duplicateName\";\nexport type AccountValidationError = CodeValidationError | NameValidationError;\n\n/**\n * Validate just the code field. Split out from the full draft\n * validator so AccountEditor can paint a per-field red border in\n * realtime without re-running the name check on every keystroke.\n */\nexport function validateCodeField(draft: AccountDraft, existing: readonly Account[], isNew: boolean): CodeValidationError | null {\n const trimmedCode = draft.code.trim();\n if (trimmedCode.length === 0) return \"emptyCode\";\n if (trimmedCode.startsWith(RESERVED_PREFIX)) return \"reservedCode\";\n // 4-digit numbering is enforced for new accounts only: pre-existing\n // books may already hold legacy codes the user added before the\n // rule landed, and changing the code would orphan their journal\n // lines (codes are immutable once created — see codeReadOnlyHint).\n if (isNew && !isValidAccountCode(trimmedCode)) return \"invalidCodeFormat\";\n if (isNew && !codeMatchesType(trimmedCode, draft.type)) return \"codeTypeMismatch\";\n if (isNew && existing.some((account) => account.code === trimmedCode)) return \"duplicateCode\";\n return null;\n}\n\n/**\n * Validate just the name field. Empty + duplicate (case-insensitive,\n * trimmed) against other accounts. On edit, the account being edited\n * is excluded from the duplicate check via `draft.code` — otherwise\n * every save would flag the user's own row as a collision.\n */\nexport function validateNameField(draft: AccountDraft, existing: readonly Account[], isNew: boolean): NameValidationError | null {\n const trimmedName = draft.name.trim();\n if (trimmedName.length === 0) return \"emptyName\";\n const folded = trimmedName.toLowerCase();\n const collides = existing.some((account) => {\n if (!isNew && account.code === draft.code.trim()) return false;\n return account.name.trim().toLowerCase() === folded;\n });\n if (collides) return \"duplicateName\";\n return null;\n}\n\n/**\n * Validate a draft about to be sent to `upsertAccount`. Returns\n * `null` on success or an error code on failure. Caller maps the\n * code to a localized message.\n *\n * `existing` is the current chart of accounts — used to detect a\n * duplicate code on a brand-new entry (otherwise the server would\n * silently overwrite the existing account, which is rarely what\n * the user typing into the \"Add account\" form intended).\n *\n * Code errors take precedence over name errors so the user fixes\n * one stable issue at a time as they type.\n */\nexport function validateAccountDraft(draft: AccountDraft, existing: readonly Account[], isNew: boolean): AccountValidationError | null {\n return validateCodeField(draft, existing, isNew) ?? validateNameField(draft, existing, isNew);\n}\n","<template>\n <!-- Inline editor used by AccountsModal both for \"Edit\" on an\n existing row and per-section \"+ Add\" buttons. The parent\n owns the open/closed state and the draft instance — this\n component is dumb, but it runs realtime per-field validation\n (red border) so the user gets feedback before clicking Save. -->\n <form\n class=\"flex flex-col gap-2 p-2 border border-blue-200 bg-blue-50/40 rounded text-sm\"\n :data-testid=\"isNew ? 'accounting-accounts-form-new' : `accounting-accounts-form-edit-${draft.code}`\"\n @submit.prevent=\"onSubmit\"\n >\n <div class=\"flex flex-wrap gap-2\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-28\">\n {{ t(\"pluginAccounting.accounts.columnCode\") }}\n <!-- New accounts: leading digit is fixed by type, so the\n editable input is restricted to the trailing 3 digits.\n The prefix span communicates the rule visually without\n needing a separate help string. -->\n <!-- The trailing-3-digit input has `outline-none bg-transparent`\n so the prefix span and the editable digits read as one\n pill. That removes the browser's default focus indicator,\n so we surface it on the wrapper via `focus-within:ring-1`\n — same shape as the name input below — to keep the field\n keyboard-discoverable (#1115 review). -->\n <div\n v-if=\"isNew\"\n :class=\"[\n 'flex items-stretch h-8 rounded border bg-white text-sm font-mono overflow-hidden',\n codeError ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-300 focus-within:ring-1 focus-within:ring-blue-500',\n ]\"\n >\n <span\n class=\"px-2 flex items-center bg-gray-100 text-gray-500 border-r border-gray-200 select-none\"\n data-testid=\"accounting-accounts-form-code-prefix\"\n >{{ codePrefix }}</span\n >\n <input\n v-model=\"codeTrailing\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"3\"\n pattern=\"\\d{3}\"\n class=\"px-2 grow w-0 outline-none bg-transparent\"\n data-testid=\"accounting-accounts-form-code\"\n @input=\"codeTouched = true\"\n />\n </div>\n <!-- Edit: code is immutable, so we display the actual stored\n value as a single disabled field rather than splitting\n prefix + trailing (legacy non-4-digit codes would\n otherwise be misrendered). -->\n <input\n v-else\n v-model=\"local.code\"\n type=\"text\"\n disabled\n class=\"h-8 px-2 rounded border border-gray-300 text-sm font-mono bg-gray-100 text-gray-500\"\n data-testid=\"accounting-accounts-form-code\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 grow min-w-[10rem]\">\n {{ t(\"pluginAccounting.accounts.columnName\") }}\n <input\n ref=\"nameInput\"\n v-model=\"local.name\"\n type=\"text\"\n :class=\"[\n 'h-8 px-2 rounded border text-sm focus:outline-none',\n nameError ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-300 focus:ring-1 focus:ring-blue-500',\n ]\"\n data-testid=\"accounting-accounts-form-name\"\n @input=\"nameTouched = true\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-32\">\n {{ t(\"pluginAccounting.accounts.columnType\") }}\n <!-- Type is locked in both modes:\n - NEW: the per-category \"+ Add\" button already chose\n it, and the suggested code is keyed off it.\n - EDIT: the type is part of the account's identity (it\n drives section placement, the code-prefix rule, and\n report categorization); changing it after the fact\n leads to surprising downstream effects. -->\n <select\n v-model=\"local.type\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white disabled:bg-gray-100 disabled:text-gray-500\"\n disabled\n data-testid=\"accounting-accounts-form-type\"\n >\n <option v-for=\"option in TYPE_OPTIONS\" :key=\"option\" :value=\"option\">\n {{ t(`pluginAccounting.accounts.typeOption.${option}`) }}\n </option>\n </select>\n </label>\n </div>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n <span\n >{{ t(\"pluginAccounting.accounts.columnNote\") }} <span class=\"text-gray-400\">{{ t(\"pluginAccounting.accounts.noteOptional\") }}</span></span\n >\n <input v-model=\"local.note\" type=\"text\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-accounts-form-note\" />\n </label>\n <p v-if=\"!isNew\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.accounts.codeReadOnlyHint\") }}</p>\n <!-- Always rendered with min-h to reserve a single line of space\n so the Save / Cancel buttons stay put as the message shows\n and clears. Field error wins over a stale parent error. -->\n <p class=\"text-xs text-red-500 min-h-[1rem]\" data-testid=\"accounting-accounts-form-error\">{{ fieldErrorMessage ?? error ?? \"\" }}</p>\n <div class=\"flex justify-end gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-accounts-form-cancel\"\n @click=\"emit('cancel')\"\n >\n {{ t(\"pluginAccounting.accounts.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-2.5 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"busy\"\n data-testid=\"accounting-accounts-form-save\"\n >\n {{ busy ? t(\"pluginAccounting.accounts.saving\") : t(\"pluginAccounting.accounts.save\") }}\n </button>\n </div>\n </form>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, reactive, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport type { Account, AccountType } from \"../api\";\nimport type { AccountDraft } from \"./accountDraft\";\nimport { ACCOUNT_TYPE_PREFIX } from \"./accountNumbering\";\nimport { validateCodeField, validateNameField, type AccountValidationError, type CodeValidationError, type NameValidationError } from \"./accountValidation\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{\n draft: AccountDraft;\n isNew: boolean;\n busy: boolean;\n error: string | null;\n existingAccounts: readonly Account[];\n}>();\nconst emit = defineEmits<{ save: [draft: AccountDraft]; cancel: [] }>();\n\nconst TYPE_OPTIONS: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\n\nconst VALIDATION_MESSAGE_KEYS: Record<AccountValidationError, string> = {\n emptyCode: \"pluginAccounting.accounts.errorEmptyCode\",\n reservedCode: \"pluginAccounting.accounts.errorReservedCode\",\n invalidCodeFormat: \"pluginAccounting.accounts.errorInvalidCodeFormat\",\n codeTypeMismatch: \"pluginAccounting.accounts.errorCodeTypeMismatch\",\n emptyName: \"pluginAccounting.accounts.errorEmptyName\",\n duplicateCode: \"pluginAccounting.accounts.errorDuplicateCode\",\n duplicateName: \"pluginAccounting.accounts.errorDuplicateName\",\n};\n\n// Local copy so the parent's `draft` ref stays untouched until the\n// user clicks Save. Cancelling reverts cleanly because the parent\n// just discards its draft.\nconst local = reactive<AccountDraft>({ ...props.draft });\nconst nameInput = ref<HTMLInputElement | null>(null);\n\n// Track which fields the user has interacted with so we can suppress\n// \"empty required\" errors on a freshly-opened editor (the suggested\n// code is always valid, but a brand-new account starts with an empty\n// name — flashing red before the user has typed would be noise).\n// Format / collision errors fire immediately because they only\n// happen when the user has actually entered something.\nconst codeTouched = ref(false);\nconst nameTouched = ref(false);\n\nconst codePrefix = computed(() => String(ACCOUNT_TYPE_PREFIX[local.type]));\n\n// Two-way binding for the trailing 3 digits. The full code\n// (`local.code`) remains the source of truth; the trailing slice is\n// derived. Non-digits and overflow are stripped on input so the\n// downstream validator only ever sees a clean 4-digit candidate.\nconst codeTrailing = computed({\n get: () => {\n const { code } = local;\n if (code.startsWith(codePrefix.value)) return code.slice(codePrefix.value.length);\n return code;\n },\n set: (val: string) => {\n const cleaned = val.replace(/\\D/g, \"\").slice(0, 3);\n local.code = codePrefix.value + cleaned;\n },\n});\n\nconst codeError = computed<CodeValidationError | null>(() => {\n const result = validateCodeField(local, props.existingAccounts, props.isNew);\n if (result === \"emptyCode\" && !codeTouched.value) return null;\n return result;\n});\n\nconst nameError = computed<NameValidationError | null>(() => {\n const result = validateNameField(local, props.existingAccounts, props.isNew);\n // For NEW accounts the empty-name field is invalid from the moment\n // the editor opens — flag it red right away to communicate the\n // requirement. For edits, the name is non-empty on open; only flag\n // emptyName once the user has actively cleared it (post-touch).\n if (result === \"emptyName\" && !nameTouched.value && !props.isNew) return null;\n return result;\n});\n\nconst fieldErrorMessage = computed<string | null>(() => {\n const code = codeError.value;\n if (code !== null) return t(VALIDATION_MESSAGE_KEYS[code]);\n const name = nameError.value;\n if (name !== null) return t(VALIDATION_MESSAGE_KEYS[name]);\n return null;\n});\n\n// Re-sync when the parent swaps which account is being edited\n// (e.g. user clicks Edit on a different row without first\n// cancelling). Single watcher rather than per-field copy to keep\n// behaviour boringly predictable.\nwatch(\n () => props.draft,\n (next) => {\n local.code = next.code;\n local.name = next.name;\n local.type = next.type;\n local.note = next.note;\n codeTouched.value = false;\n nameTouched.value = false;\n },\n);\n\nonMounted(() => {\n // Land the cursor in the field the user actually has to fill in:\n // - new accounts: code is suggested and type is locked, so\n // Name is the only non-decorative input.\n // - edits: code is disabled, type is rarely the reason for\n // editing — Name is still the most likely target. Keeping\n // focus consistent across new/edit avoids surprise.\n void nextTick(() => nameInput.value?.focus());\n});\n\nfunction onSubmit(): void {\n // Surface any latent empty-required errors that were suppressed\n // pre-touch — clicking Save is intent enough to want the red\n // border, even if the user never typed in the field.\n codeTouched.value = true;\n nameTouched.value = true;\n emit(\"save\", { code: local.code, name: local.name, type: local.type, note: local.note });\n}\n</script>\n","<template>\n <!-- Inline editor used by AccountsModal both for \"Edit\" on an\n existing row and per-section \"+ Add\" buttons. The parent\n owns the open/closed state and the draft instance — this\n component is dumb, but it runs realtime per-field validation\n (red border) so the user gets feedback before clicking Save. -->\n <form\n class=\"flex flex-col gap-2 p-2 border border-blue-200 bg-blue-50/40 rounded text-sm\"\n :data-testid=\"isNew ? 'accounting-accounts-form-new' : `accounting-accounts-form-edit-${draft.code}`\"\n @submit.prevent=\"onSubmit\"\n >\n <div class=\"flex flex-wrap gap-2\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-28\">\n {{ t(\"pluginAccounting.accounts.columnCode\") }}\n <!-- New accounts: leading digit is fixed by type, so the\n editable input is restricted to the trailing 3 digits.\n The prefix span communicates the rule visually without\n needing a separate help string. -->\n <!-- The trailing-3-digit input has `outline-none bg-transparent`\n so the prefix span and the editable digits read as one\n pill. That removes the browser's default focus indicator,\n so we surface it on the wrapper via `focus-within:ring-1`\n — same shape as the name input below — to keep the field\n keyboard-discoverable (#1115 review). -->\n <div\n v-if=\"isNew\"\n :class=\"[\n 'flex items-stretch h-8 rounded border bg-white text-sm font-mono overflow-hidden',\n codeError ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-300 focus-within:ring-1 focus-within:ring-blue-500',\n ]\"\n >\n <span\n class=\"px-2 flex items-center bg-gray-100 text-gray-500 border-r border-gray-200 select-none\"\n data-testid=\"accounting-accounts-form-code-prefix\"\n >{{ codePrefix }}</span\n >\n <input\n v-model=\"codeTrailing\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"3\"\n pattern=\"\\d{3}\"\n class=\"px-2 grow w-0 outline-none bg-transparent\"\n data-testid=\"accounting-accounts-form-code\"\n @input=\"codeTouched = true\"\n />\n </div>\n <!-- Edit: code is immutable, so we display the actual stored\n value as a single disabled field rather than splitting\n prefix + trailing (legacy non-4-digit codes would\n otherwise be misrendered). -->\n <input\n v-else\n v-model=\"local.code\"\n type=\"text\"\n disabled\n class=\"h-8 px-2 rounded border border-gray-300 text-sm font-mono bg-gray-100 text-gray-500\"\n data-testid=\"accounting-accounts-form-code\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 grow min-w-[10rem]\">\n {{ t(\"pluginAccounting.accounts.columnName\") }}\n <input\n ref=\"nameInput\"\n v-model=\"local.name\"\n type=\"text\"\n :class=\"[\n 'h-8 px-2 rounded border text-sm focus:outline-none',\n nameError ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-300 focus:ring-1 focus:ring-blue-500',\n ]\"\n data-testid=\"accounting-accounts-form-name\"\n @input=\"nameTouched = true\"\n />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-32\">\n {{ t(\"pluginAccounting.accounts.columnType\") }}\n <!-- Type is locked in both modes:\n - NEW: the per-category \"+ Add\" button already chose\n it, and the suggested code is keyed off it.\n - EDIT: the type is part of the account's identity (it\n drives section placement, the code-prefix rule, and\n report categorization); changing it after the fact\n leads to surprising downstream effects. -->\n <select\n v-model=\"local.type\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white disabled:bg-gray-100 disabled:text-gray-500\"\n disabled\n data-testid=\"accounting-accounts-form-type\"\n >\n <option v-for=\"option in TYPE_OPTIONS\" :key=\"option\" :value=\"option\">\n {{ t(`pluginAccounting.accounts.typeOption.${option}`) }}\n </option>\n </select>\n </label>\n </div>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n <span\n >{{ t(\"pluginAccounting.accounts.columnNote\") }} <span class=\"text-gray-400\">{{ t(\"pluginAccounting.accounts.noteOptional\") }}</span></span\n >\n <input v-model=\"local.note\" type=\"text\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-accounts-form-note\" />\n </label>\n <p v-if=\"!isNew\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.accounts.codeReadOnlyHint\") }}</p>\n <!-- Always rendered with min-h to reserve a single line of space\n so the Save / Cancel buttons stay put as the message shows\n and clears. Field error wins over a stale parent error. -->\n <p class=\"text-xs text-red-500 min-h-[1rem]\" data-testid=\"accounting-accounts-form-error\">{{ fieldErrorMessage ?? error ?? \"\" }}</p>\n <div class=\"flex justify-end gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-accounts-form-cancel\"\n @click=\"emit('cancel')\"\n >\n {{ t(\"pluginAccounting.accounts.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-2.5 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"busy\"\n data-testid=\"accounting-accounts-form-save\"\n >\n {{ busy ? t(\"pluginAccounting.accounts.saving\") : t(\"pluginAccounting.accounts.save\") }}\n </button>\n </div>\n </form>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, reactive, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport type { Account, AccountType } from \"../api\";\nimport type { AccountDraft } from \"./accountDraft\";\nimport { ACCOUNT_TYPE_PREFIX } from \"./accountNumbering\";\nimport { validateCodeField, validateNameField, type AccountValidationError, type CodeValidationError, type NameValidationError } from \"./accountValidation\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{\n draft: AccountDraft;\n isNew: boolean;\n busy: boolean;\n error: string | null;\n existingAccounts: readonly Account[];\n}>();\nconst emit = defineEmits<{ save: [draft: AccountDraft]; cancel: [] }>();\n\nconst TYPE_OPTIONS: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\n\nconst VALIDATION_MESSAGE_KEYS: Record<AccountValidationError, string> = {\n emptyCode: \"pluginAccounting.accounts.errorEmptyCode\",\n reservedCode: \"pluginAccounting.accounts.errorReservedCode\",\n invalidCodeFormat: \"pluginAccounting.accounts.errorInvalidCodeFormat\",\n codeTypeMismatch: \"pluginAccounting.accounts.errorCodeTypeMismatch\",\n emptyName: \"pluginAccounting.accounts.errorEmptyName\",\n duplicateCode: \"pluginAccounting.accounts.errorDuplicateCode\",\n duplicateName: \"pluginAccounting.accounts.errorDuplicateName\",\n};\n\n// Local copy so the parent's `draft` ref stays untouched until the\n// user clicks Save. Cancelling reverts cleanly because the parent\n// just discards its draft.\nconst local = reactive<AccountDraft>({ ...props.draft });\nconst nameInput = ref<HTMLInputElement | null>(null);\n\n// Track which fields the user has interacted with so we can suppress\n// \"empty required\" errors on a freshly-opened editor (the suggested\n// code is always valid, but a brand-new account starts with an empty\n// name — flashing red before the user has typed would be noise).\n// Format / collision errors fire immediately because they only\n// happen when the user has actually entered something.\nconst codeTouched = ref(false);\nconst nameTouched = ref(false);\n\nconst codePrefix = computed(() => String(ACCOUNT_TYPE_PREFIX[local.type]));\n\n// Two-way binding for the trailing 3 digits. The full code\n// (`local.code`) remains the source of truth; the trailing slice is\n// derived. Non-digits and overflow are stripped on input so the\n// downstream validator only ever sees a clean 4-digit candidate.\nconst codeTrailing = computed({\n get: () => {\n const { code } = local;\n if (code.startsWith(codePrefix.value)) return code.slice(codePrefix.value.length);\n return code;\n },\n set: (val: string) => {\n const cleaned = val.replace(/\\D/g, \"\").slice(0, 3);\n local.code = codePrefix.value + cleaned;\n },\n});\n\nconst codeError = computed<CodeValidationError | null>(() => {\n const result = validateCodeField(local, props.existingAccounts, props.isNew);\n if (result === \"emptyCode\" && !codeTouched.value) return null;\n return result;\n});\n\nconst nameError = computed<NameValidationError | null>(() => {\n const result = validateNameField(local, props.existingAccounts, props.isNew);\n // For NEW accounts the empty-name field is invalid from the moment\n // the editor opens — flag it red right away to communicate the\n // requirement. For edits, the name is non-empty on open; only flag\n // emptyName once the user has actively cleared it (post-touch).\n if (result === \"emptyName\" && !nameTouched.value && !props.isNew) return null;\n return result;\n});\n\nconst fieldErrorMessage = computed<string | null>(() => {\n const code = codeError.value;\n if (code !== null) return t(VALIDATION_MESSAGE_KEYS[code]);\n const name = nameError.value;\n if (name !== null) return t(VALIDATION_MESSAGE_KEYS[name]);\n return null;\n});\n\n// Re-sync when the parent swaps which account is being edited\n// (e.g. user clicks Edit on a different row without first\n// cancelling). Single watcher rather than per-field copy to keep\n// behaviour boringly predictable.\nwatch(\n () => props.draft,\n (next) => {\n local.code = next.code;\n local.name = next.name;\n local.type = next.type;\n local.note = next.note;\n codeTouched.value = false;\n nameTouched.value = false;\n },\n);\n\nonMounted(() => {\n // Land the cursor in the field the user actually has to fill in:\n // - new accounts: code is suggested and type is locked, so\n // Name is the only non-decorative input.\n // - edits: code is disabled, type is rarely the reason for\n // editing — Name is still the most likely target. Keeping\n // focus consistent across new/edit avoids surprise.\n void nextTick(() => nameInput.value?.focus());\n});\n\nfunction onSubmit(): void {\n // Surface any latent empty-required errors that were suppressed\n // pre-touch — clicking Save is intent enough to want the red\n // border, even if the user never typed in the field.\n codeTouched.value = true;\n nameTouched.value = true;\n emit(\"save\", { code: local.code, name: local.name, type: local.type, note: local.note });\n}\n</script>\n","<template>\n <!-- Manage-accounts modal. Opened from JournalEntryForm and\n OpeningBalancesForm. Lists the current chart of accounts\n grouped by type, with inline add / edit. Stays open across\n saves so the user can fix several accounts in a row. -->\n <div\n class=\"fixed inset-0 z-50 bg-black/20 flex items-center justify-center\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"accounting-accounts-modal-title\"\n data-testid=\"accounting-accounts-modal\"\n @click.self=\"onBackdropClick\"\n @keydown.esc=\"emit('close')\"\n >\n <div class=\"bg-white rounded shadow-lg w-[32rem] max-h-[80vh] flex flex-col\">\n <header class=\"flex items-center justify-between px-4 py-2 border-b border-gray-200 shrink-0\">\n <h3 id=\"accounting-accounts-modal-title\" class=\"text-base font-semibold\">{{ t(\"pluginAccounting.accounts.modalTitle\") }}</h3>\n <button\n ref=\"closeButton\"\n type=\"button\"\n class=\"h-8 w-8 flex items-center justify-center rounded text-gray-500 hover:bg-gray-100\"\n data-testid=\"accounting-accounts-close\"\n :aria-label=\"t('pluginAccounting.common.cancel')\"\n @click=\"emit('close')\"\n >\n <span class=\"material-icons text-base\">close</span>\n </button>\n </header>\n <div class=\"flex-1 overflow-auto px-4 py-3 flex flex-col gap-3\">\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-accounts-success\">{{ successMessage }}</p>\n <p v-if=\"toggleError\" class=\"text-xs text-red-500\" data-testid=\"accounting-accounts-toggle-error\">{{ toggleError }}</p>\n <section v-for=\"group in groups\" :key=\"group.type\" class=\"flex flex-col gap-1\">\n <h4 class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ t(`pluginAccounting.accounts.sectionTitle.${group.type}`) }}</h4>\n <div v-if=\"group.accounts.length === 0\" class=\"text-xs text-gray-400 italic px-1\">{{ t(\"pluginAccounting.common.empty\") }}</div>\n <template v-for=\"account in group.accounts\" :key=\"account.code\">\n <AccountRow v-if=\"editingCode !== account.code\" :account=\"account\" @edit=\"onEdit(account)\" @toggle-active=\"onToggleActive(account)\" />\n <AccountEditor\n v-else\n :draft=\"draft\"\n :is-new=\"false\"\n :busy=\"saving\"\n :error=\"error\"\n :existing-accounts=\"accounts\"\n @save=\"onSave\"\n @cancel=\"onCancelEditor\"\n />\n </template>\n <div v-if=\"addingNew && draft.type === group.type\" :ref=\"(node) => bindNewEditor(node, group.type)\">\n <AccountEditor :draft=\"draft\" is-new :busy=\"saving\" :error=\"error\" :existing-accounts=\"accounts\" @save=\"onSave\" @cancel=\"onCancelEditor\" />\n </div>\n <button\n v-else\n type=\"button\"\n class=\"self-start h-8 px-2.5 flex items-center gap-1 rounded text-xs text-gray-600 hover:bg-gray-100\"\n :data-testid=\"`accounting-accounts-add-${group.type}`\"\n @click=\"onAdd(group.type)\"\n >\n <span class=\"material-icons text-sm\">add</span>\n <span>{{ t(\"pluginAccounting.accounts.addToCategory\", { type: t(`pluginAccounting.accounts.typeOption.${group.type}`) }) }}</span>\n </button>\n </section>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, onUnmounted, ref } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { upsertAccount, type Account, type AccountType } from \"../api\";\nimport AccountRow from \"./AccountRow.vue\";\nimport AccountEditor from \"./AccountEditor.vue\";\nimport type { AccountDraft } from \"./accountDraft\";\nimport { validateAccountDraft, type AccountValidationError } from \"./accountValidation\";\nimport { suggestNextCode } from \"./accountNumbering\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[] }>();\nconst emit = defineEmits<{ close: []; changed: [] }>();\n\n// Order matches conventional financial-statement layout (B/S then\n// P/L). Section titles are pulled from i18n via the literal type\n// keys, so this array drives both ordering and visibility.\nconst ACCOUNT_TYPES: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\nconst SUCCESS_FADE_MS = 2500;\n\nconst VALIDATION_MESSAGE_KEYS: Record<AccountValidationError, string> = {\n emptyCode: \"pluginAccounting.accounts.errorEmptyCode\",\n reservedCode: \"pluginAccounting.accounts.errorReservedCode\",\n invalidCodeFormat: \"pluginAccounting.accounts.errorInvalidCodeFormat\",\n codeTypeMismatch: \"pluginAccounting.accounts.errorCodeTypeMismatch\",\n emptyName: \"pluginAccounting.accounts.errorEmptyName\",\n duplicateCode: \"pluginAccounting.accounts.errorDuplicateCode\",\n duplicateName: \"pluginAccounting.accounts.errorDuplicateName\",\n};\n\ninterface AccountGroup {\n type: AccountType;\n accounts: Account[];\n}\n\nconst groups = computed<AccountGroup[]>(() =>\n ACCOUNT_TYPES.map((type) => ({\n type,\n accounts: props.accounts\n .filter((account) => account.type === type)\n .slice()\n .sort(byCode),\n })),\n);\n\nfunction byCode(left: Account, right: Account): number {\n return left.code.localeCompare(right.code);\n}\n\nconst editingCode = ref<string | null>(null);\nconst addingNew = ref(false);\nconst draft = ref<AccountDraft>(emptyDraft(\"asset\"));\nconst saving = ref(false);\nconst error = ref<string | null>(null);\n// Toggle (Deactivate / Reactivate) keeps its own state. Sharing\n// `saving` / `error` with the editor would (a) hide a toggle\n// failure when no editor is mounted to render `:error`, and (b)\n// blank out an in-progress editor's validation message and\n// freeze its Save button when the user fires a toggle on a\n// different row.\nconst toggleSaving = ref(false);\nconst toggleError = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\nconst closeButton = ref<HTMLButtonElement | null>(null);\nconst newEditorWrapper = ref<HTMLDivElement | null>(null);\nlet successTimer: ReturnType<typeof setTimeout> | null = null;\n\nfunction emptyDraft(type: AccountType): AccountDraft {\n return { code: \"\", name: \"\", type, note: \"\" };\n}\n\nfunction draftForNew(type: AccountType): AccountDraft {\n return { code: suggestNextCode(type, props.accounts), name: \"\", type, note: \"\" };\n}\n\n// Vue's `:ref` on a v-for-style element gives us back either the\n// node or null (on unmount). We only want to capture the editor\n// belonging to the section that owns the current draft, so the\n// section type is checked here rather than relying on the order\n// in which Vue invokes the function refs.\nfunction bindNewEditor(node: Element | object | null, sectionType: AccountType): void {\n if (sectionType !== draft.value.type) return;\n newEditorWrapper.value = (node as HTMLDivElement | null) ?? null;\n}\n\nfunction onEdit(account: Account): void {\n // Collapse any other editor first so only one is open at a time.\n addingNew.value = false;\n error.value = null;\n draft.value = { code: account.code, name: account.name, type: account.type, note: account.note ?? \"\" };\n editingCode.value = account.code;\n}\n\nfunction onAdd(type: AccountType): void {\n editingCode.value = null;\n error.value = null;\n draft.value = draftForNew(type);\n addingNew.value = true;\n // Scroll the new in-place editor into view in case the section\n // sits below the visible viewport — opening the editor without\n // scrolling would leave the user staring at unchanged content\n // above the fold.\n void nextTick(() => {\n newEditorWrapper.value?.scrollIntoView({ behavior: \"smooth\", block: \"nearest\" });\n });\n}\n\nfunction onCancelEditor(): void {\n editingCode.value = null;\n addingNew.value = false;\n error.value = null;\n draft.value = emptyDraft(\"asset\");\n}\n\nfunction validateDraft(next: AccountDraft, isNew: boolean): string | null {\n const code = validateAccountDraft(next, props.accounts, isNew);\n return code === null ? null : t(VALIDATION_MESSAGE_KEYS[code]);\n}\n\nasync function onSave(next: AccountDraft): Promise<void> {\n if (saving.value) return;\n const isNew = addingNew.value;\n const validation = validateDraft(next, isNew);\n if (validation !== null) {\n error.value = validation;\n return;\n }\n saving.value = true;\n error.value = null;\n try {\n const account: Account = {\n code: next.code.trim(),\n name: next.name.trim(),\n type: next.type,\n };\n const note = next.note.trim();\n if (note.length > 0) account.note = note;\n // Preserve the existing active flag on edit — the editor\n // doesn't surface the field, so reading from props.accounts\n // is the only place the truth lives.\n if (!isNew) {\n const existing = props.accounts.find((entry) => entry.code === account.code);\n if (existing?.active === false) account.active = false;\n }\n const result = await upsertAccount(account, props.bookId);\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n onCancelEditor();\n showSuccess(t(\"pluginAccounting.accounts.success\"));\n emit(\"changed\");\n } catch (err) {\n // apiPost normally folds network / HTTP failures into\n // result.ok=false, so this is a belt-and-braces guard against\n // a runtime failure that would otherwise leave the Save button\n // stuck on \"Saving…\".\n error.value = errorMessage(err);\n } finally {\n saving.value = false;\n }\n}\n\nasync function onToggleActive(account: Account): Promise<void> {\n // No confirm dialog: deactivation hides the account from the\n // entry/ledger dropdowns but is fully reversible via Reactivate\n // on the same row, and historical entries are unaffected. A\n // confirm prompt was over-protective for an action that's a\n // single click to undo.\n //\n // Toggle uses its own `toggleSaving` / `toggleError` refs rather\n // than the AccountEditor's shared `saving` / `error` so that a\n // toggle failure still surfaces (via the toggle banner) when no\n // editor is mounted to render `:error`.\n if (toggleSaving.value) return;\n // Dismiss any open editor — the row about to (de)activate may be\n // the same one being edited, and even when it isn't, the user has\n // shifted attention to the toggle. Unsaved edits are dropped per\n // product call: reopening Edit is one click.\n onCancelEditor();\n const willDeactivate = account.active !== false;\n toggleSaving.value = true;\n toggleError.value = null;\n try {\n const next: Account = {\n code: account.code,\n name: account.name,\n type: account.type,\n };\n if (account.note !== undefined && account.note.length > 0) next.note = account.note;\n // Send the active flag explicitly so the server can tell\n // \"user wants to (de)activate\" apart from \"user is editing\n // and didn't mention active\" — the latter inherits the\n // existing flag and would otherwise turn Reactivate into a\n // no-op.\n next.active = !willDeactivate;\n const result = await upsertAccount(next, props.bookId);\n if (!result.ok) {\n toggleError.value = result.error;\n return;\n }\n emit(\"changed\");\n } catch (err) {\n toggleError.value = errorMessage(err);\n } finally {\n toggleSaving.value = false;\n }\n}\n\nfunction showSuccess(message: string): void {\n successMessage.value = message;\n if (successTimer !== null) clearTimeout(successTimer);\n successTimer = setTimeout(() => {\n successMessage.value = null;\n successTimer = null;\n }, SUCCESS_FADE_MS);\n}\n\nfunction onBackdropClick(): void {\n emit(\"close\");\n}\n\nonMounted(() => {\n // Initial focus on the close button so Esc / Tab work\n // immediately and the user isn't dropped into an editor field\n // they didn't ask for.\n void nextTick(() => closeButton.value?.focus());\n});\n\nonUnmounted(() => {\n if (successTimer !== null) clearTimeout(successTimer);\n});\n</script>\n","<template>\n <!-- Manage-accounts modal. Opened from JournalEntryForm and\n OpeningBalancesForm. Lists the current chart of accounts\n grouped by type, with inline add / edit. Stays open across\n saves so the user can fix several accounts in a row. -->\n <div\n class=\"fixed inset-0 z-50 bg-black/20 flex items-center justify-center\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"accounting-accounts-modal-title\"\n data-testid=\"accounting-accounts-modal\"\n @click.self=\"onBackdropClick\"\n @keydown.esc=\"emit('close')\"\n >\n <div class=\"bg-white rounded shadow-lg w-[32rem] max-h-[80vh] flex flex-col\">\n <header class=\"flex items-center justify-between px-4 py-2 border-b border-gray-200 shrink-0\">\n <h3 id=\"accounting-accounts-modal-title\" class=\"text-base font-semibold\">{{ t(\"pluginAccounting.accounts.modalTitle\") }}</h3>\n <button\n ref=\"closeButton\"\n type=\"button\"\n class=\"h-8 w-8 flex items-center justify-center rounded text-gray-500 hover:bg-gray-100\"\n data-testid=\"accounting-accounts-close\"\n :aria-label=\"t('pluginAccounting.common.cancel')\"\n @click=\"emit('close')\"\n >\n <span class=\"material-icons text-base\">close</span>\n </button>\n </header>\n <div class=\"flex-1 overflow-auto px-4 py-3 flex flex-col gap-3\">\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-accounts-success\">{{ successMessage }}</p>\n <p v-if=\"toggleError\" class=\"text-xs text-red-500\" data-testid=\"accounting-accounts-toggle-error\">{{ toggleError }}</p>\n <section v-for=\"group in groups\" :key=\"group.type\" class=\"flex flex-col gap-1\">\n <h4 class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ t(`pluginAccounting.accounts.sectionTitle.${group.type}`) }}</h4>\n <div v-if=\"group.accounts.length === 0\" class=\"text-xs text-gray-400 italic px-1\">{{ t(\"pluginAccounting.common.empty\") }}</div>\n <template v-for=\"account in group.accounts\" :key=\"account.code\">\n <AccountRow v-if=\"editingCode !== account.code\" :account=\"account\" @edit=\"onEdit(account)\" @toggle-active=\"onToggleActive(account)\" />\n <AccountEditor\n v-else\n :draft=\"draft\"\n :is-new=\"false\"\n :busy=\"saving\"\n :error=\"error\"\n :existing-accounts=\"accounts\"\n @save=\"onSave\"\n @cancel=\"onCancelEditor\"\n />\n </template>\n <div v-if=\"addingNew && draft.type === group.type\" :ref=\"(node) => bindNewEditor(node, group.type)\">\n <AccountEditor :draft=\"draft\" is-new :busy=\"saving\" :error=\"error\" :existing-accounts=\"accounts\" @save=\"onSave\" @cancel=\"onCancelEditor\" />\n </div>\n <button\n v-else\n type=\"button\"\n class=\"self-start h-8 px-2.5 flex items-center gap-1 rounded text-xs text-gray-600 hover:bg-gray-100\"\n :data-testid=\"`accounting-accounts-add-${group.type}`\"\n @click=\"onAdd(group.type)\"\n >\n <span class=\"material-icons text-sm\">add</span>\n <span>{{ t(\"pluginAccounting.accounts.addToCategory\", { type: t(`pluginAccounting.accounts.typeOption.${group.type}`) }) }}</span>\n </button>\n </section>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, onMounted, onUnmounted, ref } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { upsertAccount, type Account, type AccountType } from \"../api\";\nimport AccountRow from \"./AccountRow.vue\";\nimport AccountEditor from \"./AccountEditor.vue\";\nimport type { AccountDraft } from \"./accountDraft\";\nimport { validateAccountDraft, type AccountValidationError } from \"./accountValidation\";\nimport { suggestNextCode } from \"./accountNumbering\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[] }>();\nconst emit = defineEmits<{ close: []; changed: [] }>();\n\n// Order matches conventional financial-statement layout (B/S then\n// P/L). Section titles are pulled from i18n via the literal type\n// keys, so this array drives both ordering and visibility.\nconst ACCOUNT_TYPES: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\nconst SUCCESS_FADE_MS = 2500;\n\nconst VALIDATION_MESSAGE_KEYS: Record<AccountValidationError, string> = {\n emptyCode: \"pluginAccounting.accounts.errorEmptyCode\",\n reservedCode: \"pluginAccounting.accounts.errorReservedCode\",\n invalidCodeFormat: \"pluginAccounting.accounts.errorInvalidCodeFormat\",\n codeTypeMismatch: \"pluginAccounting.accounts.errorCodeTypeMismatch\",\n emptyName: \"pluginAccounting.accounts.errorEmptyName\",\n duplicateCode: \"pluginAccounting.accounts.errorDuplicateCode\",\n duplicateName: \"pluginAccounting.accounts.errorDuplicateName\",\n};\n\ninterface AccountGroup {\n type: AccountType;\n accounts: Account[];\n}\n\nconst groups = computed<AccountGroup[]>(() =>\n ACCOUNT_TYPES.map((type) => ({\n type,\n accounts: props.accounts\n .filter((account) => account.type === type)\n .slice()\n .sort(byCode),\n })),\n);\n\nfunction byCode(left: Account, right: Account): number {\n return left.code.localeCompare(right.code);\n}\n\nconst editingCode = ref<string | null>(null);\nconst addingNew = ref(false);\nconst draft = ref<AccountDraft>(emptyDraft(\"asset\"));\nconst saving = ref(false);\nconst error = ref<string | null>(null);\n// Toggle (Deactivate / Reactivate) keeps its own state. Sharing\n// `saving` / `error` with the editor would (a) hide a toggle\n// failure when no editor is mounted to render `:error`, and (b)\n// blank out an in-progress editor's validation message and\n// freeze its Save button when the user fires a toggle on a\n// different row.\nconst toggleSaving = ref(false);\nconst toggleError = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\nconst closeButton = ref<HTMLButtonElement | null>(null);\nconst newEditorWrapper = ref<HTMLDivElement | null>(null);\nlet successTimer: ReturnType<typeof setTimeout> | null = null;\n\nfunction emptyDraft(type: AccountType): AccountDraft {\n return { code: \"\", name: \"\", type, note: \"\" };\n}\n\nfunction draftForNew(type: AccountType): AccountDraft {\n return { code: suggestNextCode(type, props.accounts), name: \"\", type, note: \"\" };\n}\n\n// Vue's `:ref` on a v-for-style element gives us back either the\n// node or null (on unmount). We only want to capture the editor\n// belonging to the section that owns the current draft, so the\n// section type is checked here rather than relying on the order\n// in which Vue invokes the function refs.\nfunction bindNewEditor(node: Element | object | null, sectionType: AccountType): void {\n if (sectionType !== draft.value.type) return;\n newEditorWrapper.value = (node as HTMLDivElement | null) ?? null;\n}\n\nfunction onEdit(account: Account): void {\n // Collapse any other editor first so only one is open at a time.\n addingNew.value = false;\n error.value = null;\n draft.value = { code: account.code, name: account.name, type: account.type, note: account.note ?? \"\" };\n editingCode.value = account.code;\n}\n\nfunction onAdd(type: AccountType): void {\n editingCode.value = null;\n error.value = null;\n draft.value = draftForNew(type);\n addingNew.value = true;\n // Scroll the new in-place editor into view in case the section\n // sits below the visible viewport — opening the editor without\n // scrolling would leave the user staring at unchanged content\n // above the fold.\n void nextTick(() => {\n newEditorWrapper.value?.scrollIntoView({ behavior: \"smooth\", block: \"nearest\" });\n });\n}\n\nfunction onCancelEditor(): void {\n editingCode.value = null;\n addingNew.value = false;\n error.value = null;\n draft.value = emptyDraft(\"asset\");\n}\n\nfunction validateDraft(next: AccountDraft, isNew: boolean): string | null {\n const code = validateAccountDraft(next, props.accounts, isNew);\n return code === null ? null : t(VALIDATION_MESSAGE_KEYS[code]);\n}\n\nasync function onSave(next: AccountDraft): Promise<void> {\n if (saving.value) return;\n const isNew = addingNew.value;\n const validation = validateDraft(next, isNew);\n if (validation !== null) {\n error.value = validation;\n return;\n }\n saving.value = true;\n error.value = null;\n try {\n const account: Account = {\n code: next.code.trim(),\n name: next.name.trim(),\n type: next.type,\n };\n const note = next.note.trim();\n if (note.length > 0) account.note = note;\n // Preserve the existing active flag on edit — the editor\n // doesn't surface the field, so reading from props.accounts\n // is the only place the truth lives.\n if (!isNew) {\n const existing = props.accounts.find((entry) => entry.code === account.code);\n if (existing?.active === false) account.active = false;\n }\n const result = await upsertAccount(account, props.bookId);\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n onCancelEditor();\n showSuccess(t(\"pluginAccounting.accounts.success\"));\n emit(\"changed\");\n } catch (err) {\n // apiPost normally folds network / HTTP failures into\n // result.ok=false, so this is a belt-and-braces guard against\n // a runtime failure that would otherwise leave the Save button\n // stuck on \"Saving…\".\n error.value = errorMessage(err);\n } finally {\n saving.value = false;\n }\n}\n\nasync function onToggleActive(account: Account): Promise<void> {\n // No confirm dialog: deactivation hides the account from the\n // entry/ledger dropdowns but is fully reversible via Reactivate\n // on the same row, and historical entries are unaffected. A\n // confirm prompt was over-protective for an action that's a\n // single click to undo.\n //\n // Toggle uses its own `toggleSaving` / `toggleError` refs rather\n // than the AccountEditor's shared `saving` / `error` so that a\n // toggle failure still surfaces (via the toggle banner) when no\n // editor is mounted to render `:error`.\n if (toggleSaving.value) return;\n // Dismiss any open editor — the row about to (de)activate may be\n // the same one being edited, and even when it isn't, the user has\n // shifted attention to the toggle. Unsaved edits are dropped per\n // product call: reopening Edit is one click.\n onCancelEditor();\n const willDeactivate = account.active !== false;\n toggleSaving.value = true;\n toggleError.value = null;\n try {\n const next: Account = {\n code: account.code,\n name: account.name,\n type: account.type,\n };\n if (account.note !== undefined && account.note.length > 0) next.note = account.note;\n // Send the active flag explicitly so the server can tell\n // \"user wants to (de)activate\" apart from \"user is editing\n // and didn't mention active\" — the latter inherits the\n // existing flag and would otherwise turn Reactivate into a\n // no-op.\n next.active = !willDeactivate;\n const result = await upsertAccount(next, props.bookId);\n if (!result.ok) {\n toggleError.value = result.error;\n return;\n }\n emit(\"changed\");\n } catch (err) {\n toggleError.value = errorMessage(err);\n } finally {\n toggleSaving.value = false;\n }\n}\n\nfunction showSuccess(message: string): void {\n successMessage.value = message;\n if (successTimer !== null) clearTimeout(successTimer);\n successTimer = setTimeout(() => {\n successMessage.value = null;\n successTimer = null;\n }, SUCCESS_FADE_MS);\n}\n\nfunction onBackdropClick(): void {\n emit(\"close\");\n}\n\nonMounted(() => {\n // Initial focus on the close button so Esc / Tab work\n // immediately and the user isn't dropped into an editor field\n // they didn't ask for.\n void nextTick(() => closeButton.value?.focus());\n});\n\nonUnmounted(() => {\n if (successTimer !== null) clearTimeout(successTimer);\n});\n</script>\n","<template>\n <form class=\"flex flex-col gap-3\" data-testid=\"accounting-entry-form\" @submit.prevent=\"onSubmit\">\n <!-- Edit mode mounts inside the row's expanded detail panel,\n which already gives the user enough context (the row above\n shows date / kind / memo / lines). Hide the redundant\n \"Edit journal entry\" title there; the editBanner below\n still surfaces the void-and-replace consequence. -->\n <h3 v-if=\"!isEditing\" class=\"text-base font-semibold\">{{ t(\"pluginAccounting.entryForm.title\") }}</h3>\n <div class=\"flex flex-wrap gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.entryForm.dateLabel\") }}\n <input v-model=\"date\" type=\"date\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-entry-date\" />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 grow min-w-0\">\n {{ t(\"pluginAccounting.entryForm.memoLabel\") }}\n <input v-model=\"memo\" type=\"text\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-entry-memo\" />\n </label>\n </div>\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n <th v-if=\"anyTaxLine\" class=\"text-left py-1 px-2 w-40\">{{ t(\"pluginAccounting.entryForm.taxRegistrationIdLabel\") }}</th>\n <th class=\"py-1 px-2\"></th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(line, idx) in lines\" :key=\"idx\" class=\"border-b border-gray-100\">\n <td class=\"py-1 px-2\">\n <select\n v-model=\"line.accountCode\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm bg-white\"\n :data-testid=\"`accounting-entry-line-account-${idx}`\"\n >\n <option value=\"\">{{ DASH }}</option>\n <option v-for=\"account in selectableAccounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"line.debit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white\"\n :data-testid=\"`accounting-entry-line-debit-${idx}`\"\n @input=\"onDebitInput(line)\"\n />\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"line.credit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white\"\n :data-testid=\"`accounting-entry-line-credit-${idx}`\"\n @input=\"onCreditInput(line)\"\n />\n </td>\n <!-- Tax-registration ID column appears only when at least\n one line picks an input-tax account (14xx — see\n isTaxAccountCode). Within a column-visible row, the\n input itself only renders for the lines that actually\n pick a 14xx account; other lines render an empty cell\n so the row keeps its column alignment. -->\n <td v-if=\"anyTaxLine\" class=\"py-1 px-2\">\n <template v-if=\"isTaxLine(line)\">\n <input\n v-model=\"line.taxRegistrationId\"\n type=\"text\"\n :maxlength=\"MAX_TAX_REGISTRATION_ID_LENGTH\"\n :placeholder=\"t('pluginAccounting.entryForm.taxRegistrationIdPlaceholder')\"\n :class=\"[\n 'h-8 px-2 w-full rounded border text-sm font-mono bg-white focus:outline-none',\n isTaxRegistrationIdInvalid(line)\n ? 'border-red-500 ring-1 ring-red-500'\n : isTaxRegistrationIdMissing(line)\n ? 'border-amber-500 ring-1 ring-amber-500'\n : 'border-gray-300 focus:ring-1 focus:ring-blue-500',\n ]\"\n :data-testid=\"`accounting-entry-line-tax-registration-id-${idx}`\"\n :aria-describedby=\"isTaxRegistrationIdMissing(line) ? `accounting-entry-line-tax-registration-id-warning-${idx}` : undefined\"\n />\n <!-- Non-color cue for the amber border. Polite live\n region so screen readers are nudged when the\n user finishes typing an amount and the warning\n first appears, without interrupting other speech. -->\n <p\n v-if=\"isTaxRegistrationIdMissing(line)\"\n :id=\"`accounting-entry-line-tax-registration-id-warning-${idx}`\"\n class=\"text-xs text-amber-600 mt-1\"\n role=\"status\"\n aria-live=\"polite\"\n :data-testid=\"`accounting-entry-line-tax-registration-id-warning-${idx}`\"\n >\n {{ t(\"pluginAccounting.entryForm.taxRegistrationIdMissingWarning\") }}\n </p>\n </template>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <button v-if=\"lines.length > 2\" type=\"button\" class=\"text-xs text-red-500 hover:underline\" @click=\"lines.splice(idx, 1)\">\n {{ t(\"pluginAccounting.entryForm.removeLine\") }}\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n <div class=\"flex items-center justify-between\">\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-entry-add-line\"\n @click=\"addLine\"\n >\n <span class=\"material-icons text-base\">add</span>\n <span>{{ t(\"pluginAccounting.entryForm.addLine\") }}</span>\n </button>\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-entry-manage-accounts\"\n @click=\"showAccountsModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <span :class=\"balanced ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-entry-balance\">\n {{ balanced ? t(\"pluginAccounting.entryForm.balanced\") : t(\"pluginAccounting.entryForm.imbalance\", { amount: imbalanceText }) }}\n </span>\n </div>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-entry-error\">{{ error }}</p>\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-entry-success\">{{ successMessage }}</p>\n <div class=\"flex items-center justify-between gap-2\">\n <!-- editBanner sits on the action row in edit mode (instead\n of as a separate paragraph above the form) so the panel\n is shorter and the user reads the void-and-replace\n consequence right next to the button that triggers it. -->\n <p v-if=\"isEditing\" class=\"text-xs text-gray-500 flex-1 min-w-0\" data-testid=\"accounting-entry-edit-banner\">\n {{ t(\"pluginAccounting.entryForm.editBanner\") }}\n </p>\n <span v-else></span>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-3 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n :disabled=\"submitting\"\n data-testid=\"accounting-entry-cancel-edit\"\n @click=\"emit('cancel')\"\n >\n {{ t(\"pluginAccounting.common.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"!balanced || submitting || editLocked\"\n data-testid=\"accounting-entry-submit\"\n >\n {{ submitButtonLabel }}\n </button>\n </div>\n </div>\n </form>\n <!-- Sibling of the parent <form> on purpose: the modal renders\n its own <form @submit.prevent> for the inline editor, and\n nesting <form>s is invalid HTML that breaks Enter-key submit\n routing in some browsers. Vue 3 multi-root templates let us\n keep the markup flat with no wrapper div. -->\n <AccountsModal v-if=\"showAccountsModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showAccountsModal = false\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { addEntries, voidEntry, type Account, type JournalEntry, type JournalLine } from \"../api\";\nimport { formatAmount, inputStepFor, localDateString, countryHasFeature, type SupportedCountryCode } from \"../../shared\";\nimport { isTaxAccountCode } from \"./accountNumbering\";\nimport AccountsModal from \"./AccountsModal.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[]; currency: string; country?: SupportedCountryCode; entryToEdit?: JournalEntry | null }>();\nconst emit = defineEmits<{ submitted: []; cancel: [] }>();\n\nconst showAccountsModal = ref(false);\n\nconst DASH = \"—\";\n\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n return `${account.name} (${account.code})`;\n}\n\n// Hide deactivated accounts from the entry dropdown — accounting\n// integrity requires keeping them in the chart of accounts (any\n// historical journal line still references the code), but new\n// entries should not be able to land on a soft-deleted account.\nconst selectableAccounts = computed<Account[]>(() => props.accounts.filter((account) => account.active !== false));\nconst selectableAccountCodes = computed<Set<string>>(() => new Set(selectableAccounts.value.map((account) => account.code)));\n\ninterface FormLine {\n accountCode: string;\n debit: number | null;\n credit: number | null;\n taxRegistrationId: string;\n}\n\n// Mirrors server/accounting/journal.ts MAX_TAX_REGISTRATION_ID_LENGTH.\n// Duplicated rather than imported to keep the front-end bundle from\n// pulling in server modules (the existing client / server type alias\n// pattern in api.ts does the same — both sides own their copy of the\n// shape).\nconst MAX_TAX_REGISTRATION_ID_LENGTH = 32;\n\nfunction blankLine(): FormLine {\n return { accountCode: \"\", debit: null, credit: null, taxRegistrationId: \"\" };\n}\n\nfunction isTaxRegistrationIdInvalid(line: FormLine): boolean {\n return line.taxRegistrationId.trim().length > MAX_TAX_REGISTRATION_ID_LENGTH;\n}\n\nfunction isTaxLine(line: FormLine): boolean {\n return line.accountCode !== \"\" && isTaxAccountCode(line.accountCode);\n}\n\n// Soft warning: a postable tax line in a jurisdiction the role\n// prompt requires a counterparty registration number for (JP, EU,\n// GB, IN, AU, NZ, CA — see COUNTRY_FEATURES.warnMissingTaxRegistrationId)\n// gets an amber border + helper text when the field is blank. The\n// form lets the user post anyway (some suppliers genuinely won't\n// have one), but the silent-strip in `toApiLines` no longer goes\n// unnoticed. `function` declarations hoist, so calling `isPostable`\n// here is fine even though it appears later in the file.\nfunction isTaxRegistrationIdMissing(line: FormLine): boolean {\n if (!isTaxLine(line)) return false;\n if (!isPostable(line)) return false;\n if (!countryHasFeature(\"warnMissingTaxRegistrationId\", props.country)) return false;\n return line.taxRegistrationId.trim() === \"\";\n}\n\nconst date = ref(localDateString());\nconst memo = ref(\"\");\nconst lines = ref<FormLine[]>([blankLine(), blankLine()]);\nconst submitting = ref(false);\nconst error = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\n\nconst isEditing = computed<boolean>(() => Boolean(props.entryToEdit));\nconst submitButtonLabel = computed<string>(() => {\n if (submitting.value) {\n return isEditing.value ? t(\"pluginAccounting.entryForm.updating\") : t(\"pluginAccounting.entryForm.submitting\");\n }\n return isEditing.value ? t(\"pluginAccounting.entryForm.update\") : t(\"pluginAccounting.entryForm.submit\");\n});\n\n// One-shot lock: once the user has clicked Update on an edit, the\n// submit button is dead until they Cancel (or land on a different\n// entry). Edit = void + addEntries as two sequential calls; if the\n// void succeeds and the add fails, a second Submit would try to\n// void an already-voided original. We don't add retry plumbing\n// for that — policy is \"report the error, do not retry\". The user\n// cancels out and re-enters manually.\nconst editAttempted = ref(false);\nconst editLocked = computed(() => isEditing.value && editAttempted.value);\n\nfunction addLine(): void {\n lines.value.push(blankLine());\n}\n\n// Toggling ensures a single line never has both sides set — the\n// server validates this too, but doing it on input prevents a\n// confusing UX where the running total goes negative as the user\n// types.\nfunction onDebitInput(line: FormLine): void {\n if (line.debit !== null && line.debit !== 0) line.credit = null;\n}\nfunction onCreditInput(line: FormLine): void {\n if (line.credit !== null && line.credit !== 0) line.debit = null;\n}\n\n// Imbalance is computed off lines that are *postable* (have an\n// accountCode + a positive amount). Without that filter,\n// `balanced` could be `true` even when `toApiLines()` would drop a\n// row, and the user would hit a confusing \"needs ≥ 2 lines\" error\n// from the server on submit.\nconst imbalance = computed<number>(() => {\n let sum = 0;\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n if (isPositiveAmount(line.debit)) sum += line.debit;\n if (isPositiveAmount(line.credit)) sum -= line.credit;\n }\n return sum;\n});\nconst hasAtLeastTwoPostableLines = computed(() => {\n let count = 0;\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n count += 1;\n if (count >= 2) return true;\n }\n return false;\n});\n// Show the tax-registration ID column only when at least one line\n// targets a 14xx (input-tax) account; otherwise the column is\n// wasted space for the typical entry that has no input-tax line.\nconst anyTaxLine = computed(() => lines.value.some(isTaxLine));\nconst hasTaxRegistrationIdError = computed(() => lines.value.some(isTaxRegistrationIdInvalid));\nconst balanced = computed(() => Math.abs(imbalance.value) <= 0.005 && hasAtLeastTwoPostableLines.value && !hasTaxRegistrationIdError.value);\nconst imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));\nconst step = computed(() => inputStepFor(props.currency));\n\nfunction isPositiveAmount(value: unknown): value is number {\n // Robust against the empty string `v-model.number` produces when\n // the user clears a previously-typed field — `\"\" ?? 0 === 0` is\n // false so a naive truthy check would let the empty input through\n // as a phantom zero amount.\n return typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\nfunction isPostable(line: FormLine): boolean {\n if (!line.accountCode) return false;\n // Defence-in-depth against a code that was selectable when the\n // user picked it but got deactivated mid-edit. Hiding the\n // option from the dropdown alone isn't enough — the form's\n // `accountCode` value is sticky, so a stale selection would\n // still be POSTed if the user just hits submit. Gating\n // postability here also flows through to `balanced` and\n // `hasAtLeastTwoPostableLines`, so the submit button disables\n // and the user gets immediate feedback.\n if (!selectableAccountCodes.value.has(line.accountCode)) return false;\n return isPositiveAmount(line.debit) || isPositiveAmount(line.credit);\n}\n\nfunction toApiLines(): JournalLine[] {\n const out: JournalLine[] = [];\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n const apiLine: JournalLine = { accountCode: line.accountCode };\n if (isPositiveAmount(line.debit)) apiLine.debit = line.debit;\n if (isPositiveAmount(line.credit)) apiLine.credit = line.credit;\n // Only persist taxRegistrationId on tax-related lines —\n // otherwise a value typed against an earlier account choice\n // would leak through after the user switched the line to a\n // non-tax account (the input field disappears but the form\n // state lingers).\n if (isTaxLine(line)) {\n const trimmedTaxId = line.taxRegistrationId.trim();\n if (trimmedTaxId !== \"\") apiLine.taxRegistrationId = trimmedTaxId;\n }\n out.push(apiLine);\n }\n return out;\n}\n\nasync function onSubmit(): Promise<void> {\n if (submitting.value || !balanced.value || editLocked.value) return;\n submitting.value = true;\n error.value = null;\n successMessage.value = null;\n try {\n // Edit flow: void the original, then post the replacement.\n // Two sequential calls — not atomic, no retry. Marking\n // `editAttempted` *before* the void disables the submit button\n // for the rest of this edit session (the `editLocked` guard\n // and the button's :disabled both honour it), so a partial\n // failure can't trigger a second void on the already-voided\n // original. On any error: show the message, user must Cancel\n // and re-enter manually.\n const editingId = props.entryToEdit?.id;\n if (editingId) {\n editAttempted.value = true;\n const voidResult = await voidEntry({\n bookId: props.bookId,\n entryId: editingId,\n reason: t(\"pluginAccounting.entryForm.editVoidReason\"),\n });\n if (!voidResult.ok) {\n error.value = voidResult.error;\n return;\n }\n }\n const result = await addEntries({\n bookId: props.bookId,\n entries: [\n {\n date: date.value,\n memo: memo.value.trim() || undefined,\n lines: toApiLines(),\n ...(editingId ? { replacesEntryId: editingId } : {}),\n },\n ],\n });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n successMessage.value = editingId ? t(\"pluginAccounting.entryForm.editSuccess\") : t(\"pluginAccounting.entryForm.success\");\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n emit(\"submitted\");\n } catch (err) {\n // apiPost normally folds network / HTTP failures into\n // `result.ok = false`, so this branch should be rare. It's a\n // belt-and-braces guard against a runtime failure leaving the\n // submit button stuck — the user gets a visible error\n // instead of an unhandled rejection.\n error.value = errorMessage(err);\n } finally {\n submitting.value = false;\n }\n}\n\n// Reset the entire draft when bookId switches under us (rare but\n// possible via BookSwitcher while the form is open). Carrying the\n// previous book's lines and account codes into the new book is\n// the worst kind of silent failure — the new book might not even\n// have the same chart of accounts.\nwatch(\n () => props.bookId,\n () => {\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n date.value = localDateString();\n error.value = null;\n successMessage.value = null;\n },\n);\n\n// Edit mode: when the parent hands us an entry to edit, pre-fill\n// every field so the user can tweak and resubmit. When it clears\n// the prop (after submit / cancel / book switch), wipe back to a\n// blank draft so the next \"New entry\" tab visit is fresh. Mapping\n// `entry.lines` (the wire shape with optional `debit` / `credit`)\n// onto `FormLine` (which uses nullable numbers + a string\n// taxRegistrationId) is straightforward — pad missing optionals\n// to null / \"\".\nwatch(\n () => props.entryToEdit,\n (entry) => {\n error.value = null;\n successMessage.value = null;\n // Fresh edit (or exit from edit mode) → unlock the submit\n // button so the new edit session has a clean shot.\n editAttempted.value = false;\n if (!entry) {\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n date.value = localDateString();\n return;\n }\n date.value = entry.date;\n memo.value = entry.memo ?? \"\";\n lines.value = entry.lines.map((line) => ({\n accountCode: line.accountCode,\n debit: typeof line.debit === \"number\" ? line.debit : null,\n credit: typeof line.credit === \"number\" ? line.credit : null,\n taxRegistrationId: line.taxRegistrationId ?? \"\",\n }));\n if (lines.value.length < 2) {\n while (lines.value.length < 2) lines.value.push(blankLine());\n }\n },\n { immediate: true },\n);\n\n// If an account the user already picked gets deactivated mid-edit\n// (e.g. via the Manage Accounts modal in this form, or from\n// another tab via pubsub), clear the line's accountCode so the\n// <select> visibly resets to \"—\". Without this, the option is\n// gone but the form's bound value still holds the stale code,\n// which (a) leaves the user staring at a blank-looking select and\n// (b) used to slip through to submit before the isPostable guard\n// landed. Belt + suspenders.\nwatch(selectableAccountCodes, (codes) => {\n for (const line of lines.value) {\n if (line.accountCode && !codes.has(line.accountCode)) line.accountCode = \"\";\n }\n});\n</script>\n\n<style scoped>\n/* Hide the WebKit / Firefox spin buttons on amount inputs. The\n step attribute still controls validation; this is purely UI.\n Accounting amount fields don't benefit from a spinner — users\n type the number and the up/down arrows just clutter the row. */\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n appearance: textfield;\n}\n</style>\n","<template>\n <form class=\"flex flex-col gap-3\" data-testid=\"accounting-entry-form\" @submit.prevent=\"onSubmit\">\n <!-- Edit mode mounts inside the row's expanded detail panel,\n which already gives the user enough context (the row above\n shows date / kind / memo / lines). Hide the redundant\n \"Edit journal entry\" title there; the editBanner below\n still surfaces the void-and-replace consequence. -->\n <h3 v-if=\"!isEditing\" class=\"text-base font-semibold\">{{ t(\"pluginAccounting.entryForm.title\") }}</h3>\n <div class=\"flex flex-wrap gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.entryForm.dateLabel\") }}\n <input v-model=\"date\" type=\"date\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-entry-date\" />\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 grow min-w-0\">\n {{ t(\"pluginAccounting.entryForm.memoLabel\") }}\n <input v-model=\"memo\" type=\"text\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-entry-memo\" />\n </label>\n </div>\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n <th v-if=\"anyTaxLine\" class=\"text-left py-1 px-2 w-40\">{{ t(\"pluginAccounting.entryForm.taxRegistrationIdLabel\") }}</th>\n <th class=\"py-1 px-2\"></th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(line, idx) in lines\" :key=\"idx\" class=\"border-b border-gray-100\">\n <td class=\"py-1 px-2\">\n <select\n v-model=\"line.accountCode\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm bg-white\"\n :data-testid=\"`accounting-entry-line-account-${idx}`\"\n >\n <option value=\"\">{{ DASH }}</option>\n <option v-for=\"account in selectableAccounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"line.debit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white\"\n :data-testid=\"`accounting-entry-line-debit-${idx}`\"\n @input=\"onDebitInput(line)\"\n />\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"line.credit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white\"\n :data-testid=\"`accounting-entry-line-credit-${idx}`\"\n @input=\"onCreditInput(line)\"\n />\n </td>\n <!-- Tax-registration ID column appears only when at least\n one line picks an input-tax account (14xx — see\n isTaxAccountCode). Within a column-visible row, the\n input itself only renders for the lines that actually\n pick a 14xx account; other lines render an empty cell\n so the row keeps its column alignment. -->\n <td v-if=\"anyTaxLine\" class=\"py-1 px-2\">\n <template v-if=\"isTaxLine(line)\">\n <input\n v-model=\"line.taxRegistrationId\"\n type=\"text\"\n :maxlength=\"MAX_TAX_REGISTRATION_ID_LENGTH\"\n :placeholder=\"t('pluginAccounting.entryForm.taxRegistrationIdPlaceholder')\"\n :class=\"[\n 'h-8 px-2 w-full rounded border text-sm font-mono bg-white focus:outline-none',\n isTaxRegistrationIdInvalid(line)\n ? 'border-red-500 ring-1 ring-red-500'\n : isTaxRegistrationIdMissing(line)\n ? 'border-amber-500 ring-1 ring-amber-500'\n : 'border-gray-300 focus:ring-1 focus:ring-blue-500',\n ]\"\n :data-testid=\"`accounting-entry-line-tax-registration-id-${idx}`\"\n :aria-describedby=\"isTaxRegistrationIdMissing(line) ? `accounting-entry-line-tax-registration-id-warning-${idx}` : undefined\"\n />\n <!-- Non-color cue for the amber border. Polite live\n region so screen readers are nudged when the\n user finishes typing an amount and the warning\n first appears, without interrupting other speech. -->\n <p\n v-if=\"isTaxRegistrationIdMissing(line)\"\n :id=\"`accounting-entry-line-tax-registration-id-warning-${idx}`\"\n class=\"text-xs text-amber-600 mt-1\"\n role=\"status\"\n aria-live=\"polite\"\n :data-testid=\"`accounting-entry-line-tax-registration-id-warning-${idx}`\"\n >\n {{ t(\"pluginAccounting.entryForm.taxRegistrationIdMissingWarning\") }}\n </p>\n </template>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <button v-if=\"lines.length > 2\" type=\"button\" class=\"text-xs text-red-500 hover:underline\" @click=\"lines.splice(idx, 1)\">\n {{ t(\"pluginAccounting.entryForm.removeLine\") }}\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n <div class=\"flex items-center justify-between\">\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-entry-add-line\"\n @click=\"addLine\"\n >\n <span class=\"material-icons text-base\">add</span>\n <span>{{ t(\"pluginAccounting.entryForm.addLine\") }}</span>\n </button>\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-entry-manage-accounts\"\n @click=\"showAccountsModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <span :class=\"balanced ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-entry-balance\">\n {{ balanced ? t(\"pluginAccounting.entryForm.balanced\") : t(\"pluginAccounting.entryForm.imbalance\", { amount: imbalanceText }) }}\n </span>\n </div>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-entry-error\">{{ error }}</p>\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-entry-success\">{{ successMessage }}</p>\n <div class=\"flex items-center justify-between gap-2\">\n <!-- editBanner sits on the action row in edit mode (instead\n of as a separate paragraph above the form) so the panel\n is shorter and the user reads the void-and-replace\n consequence right next to the button that triggers it. -->\n <p v-if=\"isEditing\" class=\"text-xs text-gray-500 flex-1 min-w-0\" data-testid=\"accounting-entry-edit-banner\">\n {{ t(\"pluginAccounting.entryForm.editBanner\") }}\n </p>\n <span v-else></span>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-3 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n :disabled=\"submitting\"\n data-testid=\"accounting-entry-cancel-edit\"\n @click=\"emit('cancel')\"\n >\n {{ t(\"pluginAccounting.common.cancel\") }}\n </button>\n <button\n type=\"submit\"\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"!balanced || submitting || editLocked\"\n data-testid=\"accounting-entry-submit\"\n >\n {{ submitButtonLabel }}\n </button>\n </div>\n </div>\n </form>\n <!-- Sibling of the parent <form> on purpose: the modal renders\n its own <form @submit.prevent> for the inline editor, and\n nesting <form>s is invalid HTML that breaks Enter-key submit\n routing in some browsers. Vue 3 multi-root templates let us\n keep the markup flat with no wrapper div. -->\n <AccountsModal v-if=\"showAccountsModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showAccountsModal = false\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { addEntries, voidEntry, type Account, type JournalEntry, type JournalLine } from \"../api\";\nimport { formatAmount, inputStepFor, localDateString, countryHasFeature, type SupportedCountryCode } from \"../../shared\";\nimport { isTaxAccountCode } from \"./accountNumbering\";\nimport AccountsModal from \"./AccountsModal.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[]; currency: string; country?: SupportedCountryCode; entryToEdit?: JournalEntry | null }>();\nconst emit = defineEmits<{ submitted: []; cancel: [] }>();\n\nconst showAccountsModal = ref(false);\n\nconst DASH = \"—\";\n\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n return `${account.name} (${account.code})`;\n}\n\n// Hide deactivated accounts from the entry dropdown — accounting\n// integrity requires keeping them in the chart of accounts (any\n// historical journal line still references the code), but new\n// entries should not be able to land on a soft-deleted account.\nconst selectableAccounts = computed<Account[]>(() => props.accounts.filter((account) => account.active !== false));\nconst selectableAccountCodes = computed<Set<string>>(() => new Set(selectableAccounts.value.map((account) => account.code)));\n\ninterface FormLine {\n accountCode: string;\n debit: number | null;\n credit: number | null;\n taxRegistrationId: string;\n}\n\n// Mirrors server/accounting/journal.ts MAX_TAX_REGISTRATION_ID_LENGTH.\n// Duplicated rather than imported to keep the front-end bundle from\n// pulling in server modules (the existing client / server type alias\n// pattern in api.ts does the same — both sides own their copy of the\n// shape).\nconst MAX_TAX_REGISTRATION_ID_LENGTH = 32;\n\nfunction blankLine(): FormLine {\n return { accountCode: \"\", debit: null, credit: null, taxRegistrationId: \"\" };\n}\n\nfunction isTaxRegistrationIdInvalid(line: FormLine): boolean {\n return line.taxRegistrationId.trim().length > MAX_TAX_REGISTRATION_ID_LENGTH;\n}\n\nfunction isTaxLine(line: FormLine): boolean {\n return line.accountCode !== \"\" && isTaxAccountCode(line.accountCode);\n}\n\n// Soft warning: a postable tax line in a jurisdiction the role\n// prompt requires a counterparty registration number for (JP, EU,\n// GB, IN, AU, NZ, CA — see COUNTRY_FEATURES.warnMissingTaxRegistrationId)\n// gets an amber border + helper text when the field is blank. The\n// form lets the user post anyway (some suppliers genuinely won't\n// have one), but the silent-strip in `toApiLines` no longer goes\n// unnoticed. `function` declarations hoist, so calling `isPostable`\n// here is fine even though it appears later in the file.\nfunction isTaxRegistrationIdMissing(line: FormLine): boolean {\n if (!isTaxLine(line)) return false;\n if (!isPostable(line)) return false;\n if (!countryHasFeature(\"warnMissingTaxRegistrationId\", props.country)) return false;\n return line.taxRegistrationId.trim() === \"\";\n}\n\nconst date = ref(localDateString());\nconst memo = ref(\"\");\nconst lines = ref<FormLine[]>([blankLine(), blankLine()]);\nconst submitting = ref(false);\nconst error = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\n\nconst isEditing = computed<boolean>(() => Boolean(props.entryToEdit));\nconst submitButtonLabel = computed<string>(() => {\n if (submitting.value) {\n return isEditing.value ? t(\"pluginAccounting.entryForm.updating\") : t(\"pluginAccounting.entryForm.submitting\");\n }\n return isEditing.value ? t(\"pluginAccounting.entryForm.update\") : t(\"pluginAccounting.entryForm.submit\");\n});\n\n// One-shot lock: once the user has clicked Update on an edit, the\n// submit button is dead until they Cancel (or land on a different\n// entry). Edit = void + addEntries as two sequential calls; if the\n// void succeeds and the add fails, a second Submit would try to\n// void an already-voided original. We don't add retry plumbing\n// for that — policy is \"report the error, do not retry\". The user\n// cancels out and re-enters manually.\nconst editAttempted = ref(false);\nconst editLocked = computed(() => isEditing.value && editAttempted.value);\n\nfunction addLine(): void {\n lines.value.push(blankLine());\n}\n\n// Toggling ensures a single line never has both sides set — the\n// server validates this too, but doing it on input prevents a\n// confusing UX where the running total goes negative as the user\n// types.\nfunction onDebitInput(line: FormLine): void {\n if (line.debit !== null && line.debit !== 0) line.credit = null;\n}\nfunction onCreditInput(line: FormLine): void {\n if (line.credit !== null && line.credit !== 0) line.debit = null;\n}\n\n// Imbalance is computed off lines that are *postable* (have an\n// accountCode + a positive amount). Without that filter,\n// `balanced` could be `true` even when `toApiLines()` would drop a\n// row, and the user would hit a confusing \"needs ≥ 2 lines\" error\n// from the server on submit.\nconst imbalance = computed<number>(() => {\n let sum = 0;\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n if (isPositiveAmount(line.debit)) sum += line.debit;\n if (isPositiveAmount(line.credit)) sum -= line.credit;\n }\n return sum;\n});\nconst hasAtLeastTwoPostableLines = computed(() => {\n let count = 0;\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n count += 1;\n if (count >= 2) return true;\n }\n return false;\n});\n// Show the tax-registration ID column only when at least one line\n// targets a 14xx (input-tax) account; otherwise the column is\n// wasted space for the typical entry that has no input-tax line.\nconst anyTaxLine = computed(() => lines.value.some(isTaxLine));\nconst hasTaxRegistrationIdError = computed(() => lines.value.some(isTaxRegistrationIdInvalid));\nconst balanced = computed(() => Math.abs(imbalance.value) <= 0.005 && hasAtLeastTwoPostableLines.value && !hasTaxRegistrationIdError.value);\nconst imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));\nconst step = computed(() => inputStepFor(props.currency));\n\nfunction isPositiveAmount(value: unknown): value is number {\n // Robust against the empty string `v-model.number` produces when\n // the user clears a previously-typed field — `\"\" ?? 0 === 0` is\n // false so a naive truthy check would let the empty input through\n // as a phantom zero amount.\n return typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\nfunction isPostable(line: FormLine): boolean {\n if (!line.accountCode) return false;\n // Defence-in-depth against a code that was selectable when the\n // user picked it but got deactivated mid-edit. Hiding the\n // option from the dropdown alone isn't enough — the form's\n // `accountCode` value is sticky, so a stale selection would\n // still be POSTed if the user just hits submit. Gating\n // postability here also flows through to `balanced` and\n // `hasAtLeastTwoPostableLines`, so the submit button disables\n // and the user gets immediate feedback.\n if (!selectableAccountCodes.value.has(line.accountCode)) return false;\n return isPositiveAmount(line.debit) || isPositiveAmount(line.credit);\n}\n\nfunction toApiLines(): JournalLine[] {\n const out: JournalLine[] = [];\n for (const line of lines.value) {\n if (!isPostable(line)) continue;\n const apiLine: JournalLine = { accountCode: line.accountCode };\n if (isPositiveAmount(line.debit)) apiLine.debit = line.debit;\n if (isPositiveAmount(line.credit)) apiLine.credit = line.credit;\n // Only persist taxRegistrationId on tax-related lines —\n // otherwise a value typed against an earlier account choice\n // would leak through after the user switched the line to a\n // non-tax account (the input field disappears but the form\n // state lingers).\n if (isTaxLine(line)) {\n const trimmedTaxId = line.taxRegistrationId.trim();\n if (trimmedTaxId !== \"\") apiLine.taxRegistrationId = trimmedTaxId;\n }\n out.push(apiLine);\n }\n return out;\n}\n\nasync function onSubmit(): Promise<void> {\n if (submitting.value || !balanced.value || editLocked.value) return;\n submitting.value = true;\n error.value = null;\n successMessage.value = null;\n try {\n // Edit flow: void the original, then post the replacement.\n // Two sequential calls — not atomic, no retry. Marking\n // `editAttempted` *before* the void disables the submit button\n // for the rest of this edit session (the `editLocked` guard\n // and the button's :disabled both honour it), so a partial\n // failure can't trigger a second void on the already-voided\n // original. On any error: show the message, user must Cancel\n // and re-enter manually.\n const editingId = props.entryToEdit?.id;\n if (editingId) {\n editAttempted.value = true;\n const voidResult = await voidEntry({\n bookId: props.bookId,\n entryId: editingId,\n reason: t(\"pluginAccounting.entryForm.editVoidReason\"),\n });\n if (!voidResult.ok) {\n error.value = voidResult.error;\n return;\n }\n }\n const result = await addEntries({\n bookId: props.bookId,\n entries: [\n {\n date: date.value,\n memo: memo.value.trim() || undefined,\n lines: toApiLines(),\n ...(editingId ? { replacesEntryId: editingId } : {}),\n },\n ],\n });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n successMessage.value = editingId ? t(\"pluginAccounting.entryForm.editSuccess\") : t(\"pluginAccounting.entryForm.success\");\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n emit(\"submitted\");\n } catch (err) {\n // apiPost normally folds network / HTTP failures into\n // `result.ok = false`, so this branch should be rare. It's a\n // belt-and-braces guard against a runtime failure leaving the\n // submit button stuck — the user gets a visible error\n // instead of an unhandled rejection.\n error.value = errorMessage(err);\n } finally {\n submitting.value = false;\n }\n}\n\n// Reset the entire draft when bookId switches under us (rare but\n// possible via BookSwitcher while the form is open). Carrying the\n// previous book's lines and account codes into the new book is\n// the worst kind of silent failure — the new book might not even\n// have the same chart of accounts.\nwatch(\n () => props.bookId,\n () => {\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n date.value = localDateString();\n error.value = null;\n successMessage.value = null;\n },\n);\n\n// Edit mode: when the parent hands us an entry to edit, pre-fill\n// every field so the user can tweak and resubmit. When it clears\n// the prop (after submit / cancel / book switch), wipe back to a\n// blank draft so the next \"New entry\" tab visit is fresh. Mapping\n// `entry.lines` (the wire shape with optional `debit` / `credit`)\n// onto `FormLine` (which uses nullable numbers + a string\n// taxRegistrationId) is straightforward — pad missing optionals\n// to null / \"\".\nwatch(\n () => props.entryToEdit,\n (entry) => {\n error.value = null;\n successMessage.value = null;\n // Fresh edit (or exit from edit mode) → unlock the submit\n // button so the new edit session has a clean shot.\n editAttempted.value = false;\n if (!entry) {\n lines.value = [blankLine(), blankLine()];\n memo.value = \"\";\n date.value = localDateString();\n return;\n }\n date.value = entry.date;\n memo.value = entry.memo ?? \"\";\n lines.value = entry.lines.map((line) => ({\n accountCode: line.accountCode,\n debit: typeof line.debit === \"number\" ? line.debit : null,\n credit: typeof line.credit === \"number\" ? line.credit : null,\n taxRegistrationId: line.taxRegistrationId ?? \"\",\n }));\n if (lines.value.length < 2) {\n while (lines.value.length < 2) lines.value.push(blankLine());\n }\n },\n { immediate: true },\n);\n\n// If an account the user already picked gets deactivated mid-edit\n// (e.g. via the Manage Accounts modal in this form, or from\n// another tab via pubsub), clear the line's accountCode so the\n// <select> visibly resets to \"—\". Without this, the option is\n// gone but the form's bound value still holds the stale code,\n// which (a) leaves the user staring at a blank-looking select and\n// (b) used to slip through to submit before the isPostable guard\n// landed. Belt + suspenders.\nwatch(selectableAccountCodes, (codes) => {\n for (const line of lines.value) {\n if (line.accountCode && !codes.has(line.accountCode)) line.accountCode = \"\";\n }\n});\n</script>\n\n<style scoped>\n/* Hide the WebKit / Firefox spin buttons on amount inputs. The\n step attribute still controls validation; this is purely UI.\n Accounting amount fields don't benefit from a spinner — users\n type the number and the up/down arrows just clutter the row. */\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n appearance: textfield;\n}\n</style>\n","<template>\n <div class=\"flex flex-col h-full gap-3\">\n <!-- Top-row toolbar slot. Renders the embedded entry form\n in \"+ New entry\" mode here; Edit-mode for a row's existing\n entry is rendered IN-PLACE inside that row's expanded\n detail panel below. The date picker / account filter /\n table below stay visible in either state. -->\n <div v-if=\"showNewForm\" class=\"border border-gray-200 rounded p-3\" data-testid=\"accounting-journal-inline-form\">\n <JournalEntryForm\n :book-id=\"bookId\"\n :accounts=\"accounts\"\n :currency=\"currency\"\n :country=\"country\"\n :entry-to-edit=\"null\"\n @submitted=\"onFormSubmitted\"\n @cancel=\"onFormCancel\"\n />\n </div>\n <div v-else class=\"flex items-center justify-end\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-journal-new-entry\"\n @click=\"onOpenNewEntry\"\n >\n <span class=\"material-icons text-base\">add</span>\n <span>{{ t(\"pluginAccounting.tabs.newEntry\") }}</span>\n </button>\n </div>\n <div class=\"flex flex-wrap items-end gap-2\">\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.journalList.accountLabel\") }}\n <select v-model=\"accountCode\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-journal-account\">\n <option value=\"\">{{ t(\"pluginAccounting.journalList.allAccounts\") }}</option>\n <option v-for=\"account in accounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </label>\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <!-- Scrollable list area: only the entries list scrolls below\n this point. The new-entry slot + filter bar above stay\n pinned by virtue of NOT being inside this scroll container,\n and the column-header row stays visible via `position:\n sticky` on its <th>s. `min-h-0` is required for the flex-1\n child to actually shrink below its content height in a\n flex-col parent. -->\n <div class=\"flex-1 min-h-0 overflow-auto\">\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <p v-else-if=\"visibleEntries.length === 0\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.empty\") }}</p>\n <table v-else class=\"w-full text-sm\" data-testid=\"accounting-journal-table\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <!-- Per-<th> sticky (rather than `<thead class=\"sticky\">`)\n for compatibility — `position: sticky` on the\n table-header-group display is brittle in some\n browsers, but on `<th>` it's universally supported.\n `bg-white` is required so the scrolled rows beneath\n don't bleed through. -->\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.date\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.kind\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.memo\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.lines\") }}</th>\n </tr>\n </thead>\n <tbody>\n <template v-for=\"entry in visibleEntries\" :key=\"entry.id\">\n <tr\n :class=\"[\n voidedEntryIds.has(entry.id) ? 'text-gray-400 line-through' : '',\n expandedEntryId === entry.id ? 'row-selected' : '',\n 'border-b border-gray-100 align-top cursor-pointer hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400',\n ]\"\n :data-testid=\"voidedEntryIds.has(entry.id) ? `accounting-journal-row-voided-${entry.id}` : `accounting-journal-row-${entry.id}`\"\n tabindex=\"0\"\n role=\"button\"\n :aria-expanded=\"expandedEntryId === entry.id\"\n @click=\"toggleExpanded(entry.id)\"\n @keydown.enter.prevent.self=\"onKeyToggle($event, entry.id)\"\n @keydown.space.prevent.self=\"onKeyToggle($event, entry.id)\"\n >\n <td class=\"py-1 px-2 whitespace-nowrap\">{{ entry.date }}</td>\n <td class=\"py-1 px-2 text-xs\">{{ kindLabel(entry.kind) }}</td>\n <td class=\"py-1 px-2\">\n <span v-if=\"entry.memo\">{{ entry.memo }}</span>\n </td>\n <td class=\"py-1 px-2\">\n <template v-if=\"expandedEntryId !== entry.id\">\n <div v-for=\"(line, idx) in entry.lines\" :key=\"idx\" class=\"text-xs flex gap-2 items-baseline\">\n <span class=\"font-mono text-[10px] text-gray-400\">{{ line.accountCode }}</span>\n <span v-if=\"accountNameFor(line.accountCode)\">{{ accountNameFor(line.accountCode) }}</span>\n <span v-if=\"line.debit\">{{ formatDebit(line.debit) }}</span>\n <span v-if=\"line.credit\">{{ formatCredit(line.credit) }}</span>\n </div>\n </template>\n <div v-else class=\"flex items-center justify-between gap-2\">\n <span class=\"text-xs text-gray-400 font-mono\">{{ formatCreatedAt(entry.createdAt) }}</span>\n <button\n type=\"button\"\n class=\"h-6 w-6 flex items-center justify-center rounded text-gray-500 hover:bg-gray-100\"\n :data-testid=\"`accounting-journal-detail-close-${entry.id}`\"\n :aria-label=\"t('common.close')\"\n @click.stop=\"onCloseDetail\"\n >\n <span class=\"material-icons text-base\">close</span>\n </button>\n </div>\n </td>\n </tr>\n <tr v-if=\"expandedEntryId === entry.id\" class=\"bg-gray-50 detail-selected\" :data-testid=\"`accounting-journal-detail-${entry.id}`\">\n <td :colspan=\"4\" class=\"px-6 py-2\">\n <!-- Edit-in-place: the JournalEntryForm replaces the\n read-only detail content for this row when the\n user clicks Edit. Submit / cancel collapses back\n (submit also voids the original, so we clear the\n selection); top-bar \"+ New entry\" stays a separate\n path that opens the same form above the table. -->\n <div v-if=\"entryBeingEdited?.id === entry.id\" :data-testid=\"`accounting-journal-detail-edit-${entry.id}`\">\n <JournalEntryForm\n :book-id=\"bookId\"\n :accounts=\"accounts\"\n :currency=\"currency\"\n :country=\"country\"\n :entry-to-edit=\"entryBeingEdited\"\n @submitted=\"onFormSubmitted\"\n @cancel=\"onFormCancel\"\n />\n </div>\n <template v-else>\n <div class=\"flex items-center gap-3 mb-2\">\n <template v-if=\"entry.kind === 'normal' && !voidedEntryIds.has(entry.id)\">\n <button class=\"text-xs text-blue-600 hover:underline\" :data-testid=\"`accounting-edit-${entry.id}`\" @click=\"onEditEntry(entry)\">\n {{ t(\"pluginAccounting.journalList.edit\") }}\n </button>\n <button class=\"text-xs text-red-500 hover:underline\" :data-testid=\"`accounting-void-${entry.id}`\" @click=\"onVoid(entry)\">\n {{ t(\"pluginAccounting.journalList.void\") }}\n </button>\n </template>\n <button\n v-else-if=\"entry.kind === 'opening' && !voidedEntryIds.has(entry.id)\"\n class=\"text-xs text-blue-600 hover:underline\"\n :data-testid=\"`accounting-edit-opening-${entry.id}`\"\n @click=\"emit('editOpening')\"\n >\n {{ t(\"pluginAccounting.journalList.edit\") }}\n </button>\n </div>\n <table class=\"w-full text-xs\">\n <thead>\n <tr class=\"text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.memoLabel\") }}</th>\n <th v-if=\"entryHasTaxIds(entry)\" class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.taxRegistrationIdLabel\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(line, idx) in entry.lines\" :key=\"idx\" class=\"border-b border-gray-100 text-gray-700\">\n <td class=\"py-1 px-2\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ line.accountCode }}</span>\n <span v-if=\"accountNameFor(line.accountCode)\">{{ accountNameFor(line.accountCode) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">\n <template v-if=\"line.debit\">{{ formatAmount(line.debit, currency) }}</template>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">\n <template v-if=\"line.credit\">{{ formatAmount(line.credit, currency) }}</template>\n </td>\n <td class=\"py-1 px-2\">\n <template v-if=\"line.memo\">{{ line.memo }}</template>\n </td>\n <td v-if=\"entryHasTaxIds(entry)\" class=\"py-1 px-2 font-mono text-[10px]\">\n <template v-if=\"line.taxRegistrationId\">{{ line.taxRegistrationId }}</template>\n </td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300 text-gray-700\">\n <td class=\"py-1 px-2 text-gray-500\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(entryDebitTotal(entry), currency) }}</td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(entryCreditTotal(entry), currency) }}</td>\n <td :colspan=\"entryHasTaxIds(entry) ? 2 : 1\"></td>\n </tr>\n </tfoot>\n </table>\n </template>\n </td>\n </tr>\n </template>\n </tbody>\n </table>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { getJournalEntries, voidEntry, type Account, type JournalEntry, type JournalEntryKind, type JournalLine } from \"../api\";\nimport { formatAmount, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd, type SupportedCountryCode } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\nimport JournalEntryForm from \"./JournalEntryForm.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{\n bookId: string;\n accounts: Account[];\n currency: string;\n country?: SupportedCountryCode;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n /** Entry id to auto-expand and scroll into view. Surfaced by the\n * parent when an `addEntries` tool result lands so the user sees\n * the freshly-posted row highlighted. Captured into\n * `pendingPreselectId` and consumed once the entry actually\n * appears in the fetched list — refetch can race the prop. */\n preselectEntryId?: string;\n}>();\nconst emit = defineEmits<{ editOpening: []; preselectConsumed: [] }>();\n\n// Inline-form state. Two distinct surfaces, one component:\n// • showNewForm = true → blank draft, rendered above the table\n// where the \"+ New entry\" button used to be.\n// • entryBeingEdited != null → edit mode, rendered IN-PLACE inside\n// the matching row's expanded detail panel (replacing the read-\n// only debit/credit table for that row).\n// `<JournalEntryForm>` looks at `entryToEdit` to decide its title /\n// submit label; the top-bar instance always passes null.\nconst showNewForm = ref(false);\nconst entryBeingEdited = ref<JournalEntry | null>(null);\n// Single-selection detail expansion. Clicking a row swaps the\n// selection (or collapses if it's already the selected row).\n// Cleared on book switch via the closeForm watcher; entries deleted\n// between fetches simply drop out of filteredEntries, so a stale id\n// here just renders no detail row. Declared early so the\n// onFormSubmitted / bookId-watcher callbacks below can reference it.\nconst expandedEntryId = ref<string | null>(null);\n\nfunction onOpenNewEntry(): void {\n entryBeingEdited.value = null;\n showNewForm.value = true;\n}\n\nfunction onEditEntry(entry: JournalEntry): void {\n showNewForm.value = false;\n entryBeingEdited.value = entry;\n}\n\nfunction closeForm(): void {\n showNewForm.value = false;\n entryBeingEdited.value = null;\n}\n\nfunction onFormSubmitted(): void {\n // Submit posts via the form. In production the server-side\n // publishBookChange round-trips an SSE event that bumps\n // `bookVersion` and re-runs `refresh` via the watcher below.\n // We also kick a synchronous refetch here so the freshly-posted\n // row shows up immediately — the SSE round-trip can race the\n // tab repaint, and skipping it here also makes the e2e mock\n // path (no pubsub replay) deterministic.\n closeForm();\n // After an in-place edit submit, the original entry is voided\n // and replaced. Collapse the detail panel since it was pointing\n // at an entry that's now superseded.\n expandedEntryId.value = null;\n void refresh();\n}\n\nfunction onFormCancel(): void {\n closeForm();\n}\n\n// Switching books mid-edit would carry the prior book's draft into\n// the new book. Force the panel closed so the next visit starts\n// from a blank toolbar — the form's own bookId watcher would also\n// reset its internal state, but we want the user back in the\n// neutral \"+ New entry\" surface.\nwatch(\n () => props.bookId,\n () => {\n closeForm();\n expandedEntryId.value = null;\n },\n);\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\n// Default = current fiscal year. Reset by the bookId/fiscalYearEnd\n// watcher so switching books or changing the FY-end in settings\n// drops a stale custom range from the prior book.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\nconst accountCode = ref(\"\");\nconst entries = ref<JournalEntry[]>([]);\nconst serverVoidedIds = ref<string[]>([]);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction kindLabel(kind: JournalEntryKind): string {\n if (kind === \"opening\") return t(\"pluginAccounting.journalList.kind.opening\");\n if (kind === \"void\") return t(\"pluginAccounting.journalList.kind.void\");\n if (kind === \"void-marker\") return t(\"pluginAccounting.journalList.kind.voidMarker\");\n return t(\"pluginAccounting.journalList.kind.normal\");\n}\n\nfunction formatDebit(value: number): string {\n return `DR ${formatAmount(value, props.currency)}`;\n}\nfunction formatCredit(value: number): string {\n return `CR ${formatAmount(value, props.currency)}`;\n}\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n // Same convention used by JournalEntryForm and Ledger pickers.\n return `${account.name} (${account.code})`;\n}\n// `entry.createdAt` is server-stamped ISO 8601. We render local\n// date+time (no seconds, no timezone) in YYYY-MM-DD HH:MM form to\n// match `entry.date`'s style and keep the line compact. Parens are\n// baked in here so the template doesn't carry raw text (the\n// vue-i18n/no-raw-text rule flags literal strings in mustache).\nfunction formatCreatedAt(iso: string): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return `(${iso})`;\n const pad = (num: number): string => String(num).padStart(2, \"0\");\n return `(${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())})`;\n}\nconst accountNameByCode = computed(() => {\n const map = new Map<string, string>();\n for (const account of props.accounts) map.set(account.code, account.name);\n return map;\n});\nfunction accountNameFor(code: string): string | null {\n return accountNameByCode.value.get(code) ?? null;\n}\n\n// Close button on the selected row's lines cell. Has to clear BOTH\n// expandedEntryId AND entryBeingEdited — if the user clicks Edit\n// (which sets entryBeingEdited) and then clicks Close, leaving\n// entryBeingEdited stale would block reopening: toggleExpanded's\n// edit-mode guard early-returns when entryBeingEdited.id matches the\n// clicked row, so the user could never reopen that entry from the\n// list. Issue surfaced by the CodeRabbit review on PR #1161.\nfunction onCloseDetail(): void {\n expandedEntryId.value = null;\n entryBeingEdited.value = null;\n}\n\nfunction toggleExpanded(entryId: string): void {\n // While the row is in edit mode for itself, ignore clicks on the\n // row chrome (date / kind / memo / lines cells) — the user is\n // actively typing into the form below and a stray cell click\n // shouldn't collapse the panel. Cancel / Submit on the form, or\n // clicking a different row, are the deliberate exits.\n if (entryBeingEdited.value?.id === entryId) return;\n expandedEntryId.value = expandedEntryId.value === entryId ? null : entryId;\n // Switching to a different row (or collapsing) drops any\n // in-progress edit on the prior row.\n entryBeingEdited.value = null;\n}\n\nfunction onKeyToggle(event: KeyboardEvent, entryId: string): void {\n if (event.repeat) return;\n toggleExpanded(entryId);\n}\n\nfunction entryHasTaxIds(entry: JournalEntry): boolean {\n return entry.lines.some((line) => Boolean(line.taxRegistrationId));\n}\n\nfunction sumLines(lines: JournalLine[], pick: (line: JournalLine) => number | undefined): number {\n return lines.reduce((acc, line) => acc + (pick(line) ?? 0), 0);\n}\n\nfunction entryDebitTotal(entry: JournalEntry): number {\n return sumLines(entry.lines, (line) => line.debit);\n}\n\nfunction entryCreditTotal(entry: JournalEntry): number {\n return sumLines(entry.lines, (line) => line.credit);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n const result = await getJournalEntries({\n bookId: props.bookId,\n from: range.value.from || undefined,\n to: range.value.to || undefined,\n accountCode: accountCode.value || undefined,\n });\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n entries.value = [];\n serverVoidedIds.value = [];\n return;\n }\n entries.value = result.data.entries;\n serverVoidedIds.value = result.data.voidedEntryIds;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nconst filteredEntries = computed(() => entries.value);\n\n// Visible-list view that pins the entry currently being edited at\n// the top when a filter change or pubsub-driven refetch would\n// otherwise drop it from `filteredEntries`. Without this, the\n// in-place edit form (which is nested under the row's v-if /\n// v-for) would unmount and silently discard the user's draft when:\n// • the user adjusts the date range or account filter,\n// • a sibling tab / LLM tool voids the entry out-of-band and the\n// SSE pubsub bumps `bookVersion`, refetching this list,\n// • or a sibling tab / LLM tool deletes the underlying book.\n// Pinning the editing entry from the local snapshot (entryBeingEdited)\n// keeps the form mounted across all three. The pinned row sits at\n// the top of the table while editing; on submit / cancel the\n// snapshot clears and the list reverts to filteredEntries.\nconst visibleEntries = computed<JournalEntry[]>(() => {\n const list = filteredEntries.value;\n const editing = entryBeingEdited.value;\n if (editing && !list.some((entry) => entry.id === editing.id)) {\n return [editing, ...list];\n }\n return list;\n});\n\n// Set of original entry ids that have been voided. The server\n// computes this from the *unfiltered* journal (so an account-filtered\n// query — which drops void-marker rows because they have no lines —\n// still strikes out the cancelled original). Source of truth on the\n// server is `voidedIdSet()` in journal.ts.\nconst voidedEntryIds = computed(() => new Set(serverVoidedIds.value));\n\nasync function onVoid(entry: JournalEntry): Promise<void> {\n // Single dialog: the prompt is the confirmation. Cancelling\n // (returning null) cancels the void; entering empty text or a\n // reason proceeds.\n const reason = window.prompt(t(\"pluginAccounting.journalList.voidReason\"));\n if (reason === null) return;\n try {\n const result = await voidEntry({ entryId: entry.id, reason: reason || undefined, bookId: props.bookId });\n if (!result.ok) error.value = result.error;\n } catch (err) {\n error.value = errorMessage(err);\n }\n}\n\n// Reset to current-year window whenever the active book or its\n// fiscal-year end changes. Keeps a custom range from leaking across\n// books and follows a settings-driven shift in fiscalYearEnd.\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\nwatch(() => [props.bookId, props.version, range.value.from, range.value.to, accountCode.value], refresh, { immediate: true });\n\n// Pending preselect: the parent hands us an id via `preselectEntryId`,\n// but the matching entry may not be in `entries` yet (the SSE-driven\n// refetch lands on its own clock). Stash it here, then the\n// [pendingPreselectId, entries] watcher below consumes it once the\n// row actually exists in the list — and clears it so subsequent\n// unrelated refetches (void events, sibling-tab edits) don't\n// re-expand a stale target.\nconst pendingPreselectId = ref<string | null>(null);\n\nwatch(\n () => props.preselectEntryId,\n (incoming) => {\n if (incoming) pendingPreselectId.value = incoming;\n },\n // immediate: true so a late JournalList mount (the View defers our\n // mount until refetchBooks resolves activeBookId) still captures\n // a preselect the parent had already set — without this, a normal\n // watcher misses the \"initial value is the target value\" case.\n { immediate: true },\n);\n\nwatch([pendingPreselectId, entries], async ([targetId, list]) => {\n if (!targetId) return;\n if (!list.some((entry) => entry.id === targetId)) return;\n // Always emit `preselectConsumed` (whether we expand or bail) so\n // the parent can drop its `journalPreselectEntryId` ref. Without\n // this one-shot signal, leaving and returning to the journal tab\n // (v-if remount) replays the immediate prop watcher against the\n // stale value, re-expanding an old row the user has already moved\n // past. Issue raised by the Codex automated review on PR #1158.\n if (entryBeingEdited.value) {\n // Don't overwrite an in-progress edit on another row — the\n // user's draft matters more than the highlight. Drop pending so\n // we don't keep retrying every refetch, and signal consumed so\n // the parent doesn't keep re-handing us the same id.\n pendingPreselectId.value = null;\n emit(\"preselectConsumed\");\n return;\n }\n expandedEntryId.value = targetId;\n await nextTick();\n const row =\n document.querySelector(`[data-testid=\"accounting-journal-row-${targetId}\"]`) ??\n document.querySelector(`[data-testid=\"accounting-journal-row-voided-${targetId}\"]`);\n row?.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n pendingPreselectId.value = null;\n emit(\"preselectConsumed\");\n});\n</script>\n\n<style scoped>\n/* Selection frame for the expanded entry. Borders go on the cells\n (not the <tr>) because border-collapse: collapse — Tailwind's\n default — eats <tr>-level borders/box-shadows. The entry row owns\n top/left/right; the detail-panel row directly below owns\n left/right/bottom, so together they read as one rectangle around\n the selection. Color matches the focus-ring blue used elsewhere\n in this list. */\n.row-selected > td {\n background-color: rgb(239 246 255); /* tailwind blue-50 */\n border-top: 2px solid rgb(59 130 246); /* tailwind blue-500 */\n}\n.row-selected > td:first-child {\n border-left: 2px solid rgb(59 130 246);\n}\n.row-selected > td:last-child {\n border-right: 2px solid rgb(59 130 246);\n}\n.detail-selected > td {\n background-color: rgb(239 246 255);\n border-left: 2px solid rgb(59 130 246);\n border-right: 2px solid rgb(59 130 246);\n border-bottom: 2px solid rgb(59 130 246);\n}\n</style>\n","<template>\n <div class=\"flex flex-col h-full gap-3\">\n <!-- Top-row toolbar slot. Renders the embedded entry form\n in \"+ New entry\" mode here; Edit-mode for a row's existing\n entry is rendered IN-PLACE inside that row's expanded\n detail panel below. The date picker / account filter /\n table below stay visible in either state. -->\n <div v-if=\"showNewForm\" class=\"border border-gray-200 rounded p-3\" data-testid=\"accounting-journal-inline-form\">\n <JournalEntryForm\n :book-id=\"bookId\"\n :accounts=\"accounts\"\n :currency=\"currency\"\n :country=\"country\"\n :entry-to-edit=\"null\"\n @submitted=\"onFormSubmitted\"\n @cancel=\"onFormCancel\"\n />\n </div>\n <div v-else class=\"flex items-center justify-end\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-journal-new-entry\"\n @click=\"onOpenNewEntry\"\n >\n <span class=\"material-icons text-base\">add</span>\n <span>{{ t(\"pluginAccounting.tabs.newEntry\") }}</span>\n </button>\n </div>\n <div class=\"flex flex-wrap items-end gap-2\">\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.journalList.accountLabel\") }}\n <select v-model=\"accountCode\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-journal-account\">\n <option value=\"\">{{ t(\"pluginAccounting.journalList.allAccounts\") }}</option>\n <option v-for=\"account in accounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </label>\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <!-- Scrollable list area: only the entries list scrolls below\n this point. The new-entry slot + filter bar above stay\n pinned by virtue of NOT being inside this scroll container,\n and the column-header row stays visible via `position:\n sticky` on its <th>s. `min-h-0` is required for the flex-1\n child to actually shrink below its content height in a\n flex-col parent. -->\n <div class=\"flex-1 min-h-0 overflow-auto\">\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <p v-else-if=\"visibleEntries.length === 0\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.empty\") }}</p>\n <table v-else class=\"w-full text-sm\" data-testid=\"accounting-journal-table\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <!-- Per-<th> sticky (rather than `<thead class=\"sticky\">`)\n for compatibility — `position: sticky` on the\n table-header-group display is brittle in some\n browsers, but on `<th>` it's universally supported.\n `bg-white` is required so the scrolled rows beneath\n don't bleed through. -->\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.date\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.kind\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.memo\") }}</th>\n <th class=\"sticky top-0 bg-white text-left py-1 px-2\">{{ t(\"pluginAccounting.journalList.columns.lines\") }}</th>\n </tr>\n </thead>\n <tbody>\n <template v-for=\"entry in visibleEntries\" :key=\"entry.id\">\n <tr\n :class=\"[\n voidedEntryIds.has(entry.id) ? 'text-gray-400 line-through' : '',\n expandedEntryId === entry.id ? 'row-selected' : '',\n 'border-b border-gray-100 align-top cursor-pointer hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400',\n ]\"\n :data-testid=\"voidedEntryIds.has(entry.id) ? `accounting-journal-row-voided-${entry.id}` : `accounting-journal-row-${entry.id}`\"\n tabindex=\"0\"\n role=\"button\"\n :aria-expanded=\"expandedEntryId === entry.id\"\n @click=\"toggleExpanded(entry.id)\"\n @keydown.enter.prevent.self=\"onKeyToggle($event, entry.id)\"\n @keydown.space.prevent.self=\"onKeyToggle($event, entry.id)\"\n >\n <td class=\"py-1 px-2 whitespace-nowrap\">{{ entry.date }}</td>\n <td class=\"py-1 px-2 text-xs\">{{ kindLabel(entry.kind) }}</td>\n <td class=\"py-1 px-2\">\n <span v-if=\"entry.memo\">{{ entry.memo }}</span>\n </td>\n <td class=\"py-1 px-2\">\n <template v-if=\"expandedEntryId !== entry.id\">\n <div v-for=\"(line, idx) in entry.lines\" :key=\"idx\" class=\"text-xs flex gap-2 items-baseline\">\n <span class=\"font-mono text-[10px] text-gray-400\">{{ line.accountCode }}</span>\n <span v-if=\"accountNameFor(line.accountCode)\">{{ accountNameFor(line.accountCode) }}</span>\n <span v-if=\"line.debit\">{{ formatDebit(line.debit) }}</span>\n <span v-if=\"line.credit\">{{ formatCredit(line.credit) }}</span>\n </div>\n </template>\n <div v-else class=\"flex items-center justify-between gap-2\">\n <span class=\"text-xs text-gray-400 font-mono\">{{ formatCreatedAt(entry.createdAt) }}</span>\n <button\n type=\"button\"\n class=\"h-6 w-6 flex items-center justify-center rounded text-gray-500 hover:bg-gray-100\"\n :data-testid=\"`accounting-journal-detail-close-${entry.id}`\"\n :aria-label=\"t('common.close')\"\n @click.stop=\"onCloseDetail\"\n >\n <span class=\"material-icons text-base\">close</span>\n </button>\n </div>\n </td>\n </tr>\n <tr v-if=\"expandedEntryId === entry.id\" class=\"bg-gray-50 detail-selected\" :data-testid=\"`accounting-journal-detail-${entry.id}`\">\n <td :colspan=\"4\" class=\"px-6 py-2\">\n <!-- Edit-in-place: the JournalEntryForm replaces the\n read-only detail content for this row when the\n user clicks Edit. Submit / cancel collapses back\n (submit also voids the original, so we clear the\n selection); top-bar \"+ New entry\" stays a separate\n path that opens the same form above the table. -->\n <div v-if=\"entryBeingEdited?.id === entry.id\" :data-testid=\"`accounting-journal-detail-edit-${entry.id}`\">\n <JournalEntryForm\n :book-id=\"bookId\"\n :accounts=\"accounts\"\n :currency=\"currency\"\n :country=\"country\"\n :entry-to-edit=\"entryBeingEdited\"\n @submitted=\"onFormSubmitted\"\n @cancel=\"onFormCancel\"\n />\n </div>\n <template v-else>\n <div class=\"flex items-center gap-3 mb-2\">\n <template v-if=\"entry.kind === 'normal' && !voidedEntryIds.has(entry.id)\">\n <button class=\"text-xs text-blue-600 hover:underline\" :data-testid=\"`accounting-edit-${entry.id}`\" @click=\"onEditEntry(entry)\">\n {{ t(\"pluginAccounting.journalList.edit\") }}\n </button>\n <button class=\"text-xs text-red-500 hover:underline\" :data-testid=\"`accounting-void-${entry.id}`\" @click=\"onVoid(entry)\">\n {{ t(\"pluginAccounting.journalList.void\") }}\n </button>\n </template>\n <button\n v-else-if=\"entry.kind === 'opening' && !voidedEntryIds.has(entry.id)\"\n class=\"text-xs text-blue-600 hover:underline\"\n :data-testid=\"`accounting-edit-opening-${entry.id}`\"\n @click=\"emit('editOpening')\"\n >\n {{ t(\"pluginAccounting.journalList.edit\") }}\n </button>\n </div>\n <table class=\"w-full text-xs\">\n <thead>\n <tr class=\"text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.memoLabel\") }}</th>\n <th v-if=\"entryHasTaxIds(entry)\" class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.taxRegistrationIdLabel\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(line, idx) in entry.lines\" :key=\"idx\" class=\"border-b border-gray-100 text-gray-700\">\n <td class=\"py-1 px-2\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ line.accountCode }}</span>\n <span v-if=\"accountNameFor(line.accountCode)\">{{ accountNameFor(line.accountCode) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">\n <template v-if=\"line.debit\">{{ formatAmount(line.debit, currency) }}</template>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">\n <template v-if=\"line.credit\">{{ formatAmount(line.credit, currency) }}</template>\n </td>\n <td class=\"py-1 px-2\">\n <template v-if=\"line.memo\">{{ line.memo }}</template>\n </td>\n <td v-if=\"entryHasTaxIds(entry)\" class=\"py-1 px-2 font-mono text-[10px]\">\n <template v-if=\"line.taxRegistrationId\">{{ line.taxRegistrationId }}</template>\n </td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300 text-gray-700\">\n <td class=\"py-1 px-2 text-gray-500\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(entryDebitTotal(entry), currency) }}</td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(entryCreditTotal(entry), currency) }}</td>\n <td :colspan=\"entryHasTaxIds(entry) ? 2 : 1\"></td>\n </tr>\n </tfoot>\n </table>\n </template>\n </td>\n </tr>\n </template>\n </tbody>\n </table>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { getJournalEntries, voidEntry, type Account, type JournalEntry, type JournalEntryKind, type JournalLine } from \"../api\";\nimport { formatAmount, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd, type SupportedCountryCode } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\nimport JournalEntryForm from \"./JournalEntryForm.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{\n bookId: string;\n accounts: Account[];\n currency: string;\n country?: SupportedCountryCode;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n /** Entry id to auto-expand and scroll into view. Surfaced by the\n * parent when an `addEntries` tool result lands so the user sees\n * the freshly-posted row highlighted. Captured into\n * `pendingPreselectId` and consumed once the entry actually\n * appears in the fetched list — refetch can race the prop. */\n preselectEntryId?: string;\n}>();\nconst emit = defineEmits<{ editOpening: []; preselectConsumed: [] }>();\n\n// Inline-form state. Two distinct surfaces, one component:\n// • showNewForm = true → blank draft, rendered above the table\n// where the \"+ New entry\" button used to be.\n// • entryBeingEdited != null → edit mode, rendered IN-PLACE inside\n// the matching row's expanded detail panel (replacing the read-\n// only debit/credit table for that row).\n// `<JournalEntryForm>` looks at `entryToEdit` to decide its title /\n// submit label; the top-bar instance always passes null.\nconst showNewForm = ref(false);\nconst entryBeingEdited = ref<JournalEntry | null>(null);\n// Single-selection detail expansion. Clicking a row swaps the\n// selection (or collapses if it's already the selected row).\n// Cleared on book switch via the closeForm watcher; entries deleted\n// between fetches simply drop out of filteredEntries, so a stale id\n// here just renders no detail row. Declared early so the\n// onFormSubmitted / bookId-watcher callbacks below can reference it.\nconst expandedEntryId = ref<string | null>(null);\n\nfunction onOpenNewEntry(): void {\n entryBeingEdited.value = null;\n showNewForm.value = true;\n}\n\nfunction onEditEntry(entry: JournalEntry): void {\n showNewForm.value = false;\n entryBeingEdited.value = entry;\n}\n\nfunction closeForm(): void {\n showNewForm.value = false;\n entryBeingEdited.value = null;\n}\n\nfunction onFormSubmitted(): void {\n // Submit posts via the form. In production the server-side\n // publishBookChange round-trips an SSE event that bumps\n // `bookVersion` and re-runs `refresh` via the watcher below.\n // We also kick a synchronous refetch here so the freshly-posted\n // row shows up immediately — the SSE round-trip can race the\n // tab repaint, and skipping it here also makes the e2e mock\n // path (no pubsub replay) deterministic.\n closeForm();\n // After an in-place edit submit, the original entry is voided\n // and replaced. Collapse the detail panel since it was pointing\n // at an entry that's now superseded.\n expandedEntryId.value = null;\n void refresh();\n}\n\nfunction onFormCancel(): void {\n closeForm();\n}\n\n// Switching books mid-edit would carry the prior book's draft into\n// the new book. Force the panel closed so the next visit starts\n// from a blank toolbar — the form's own bookId watcher would also\n// reset its internal state, but we want the user back in the\n// neutral \"+ New entry\" surface.\nwatch(\n () => props.bookId,\n () => {\n closeForm();\n expandedEntryId.value = null;\n },\n);\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\n// Default = current fiscal year. Reset by the bookId/fiscalYearEnd\n// watcher so switching books or changing the FY-end in settings\n// drops a stale custom range from the prior book.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\nconst accountCode = ref(\"\");\nconst entries = ref<JournalEntry[]>([]);\nconst serverVoidedIds = ref<string[]>([]);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction kindLabel(kind: JournalEntryKind): string {\n if (kind === \"opening\") return t(\"pluginAccounting.journalList.kind.opening\");\n if (kind === \"void\") return t(\"pluginAccounting.journalList.kind.void\");\n if (kind === \"void-marker\") return t(\"pluginAccounting.journalList.kind.voidMarker\");\n return t(\"pluginAccounting.journalList.kind.normal\");\n}\n\nfunction formatDebit(value: number): string {\n return `DR ${formatAmount(value, props.currency)}`;\n}\nfunction formatCredit(value: number): string {\n return `CR ${formatAmount(value, props.currency)}`;\n}\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n // Same convention used by JournalEntryForm and Ledger pickers.\n return `${account.name} (${account.code})`;\n}\n// `entry.createdAt` is server-stamped ISO 8601. We render local\n// date+time (no seconds, no timezone) in YYYY-MM-DD HH:MM form to\n// match `entry.date`'s style and keep the line compact. Parens are\n// baked in here so the template doesn't carry raw text (the\n// vue-i18n/no-raw-text rule flags literal strings in mustache).\nfunction formatCreatedAt(iso: string): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return `(${iso})`;\n const pad = (num: number): string => String(num).padStart(2, \"0\");\n return `(${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())})`;\n}\nconst accountNameByCode = computed(() => {\n const map = new Map<string, string>();\n for (const account of props.accounts) map.set(account.code, account.name);\n return map;\n});\nfunction accountNameFor(code: string): string | null {\n return accountNameByCode.value.get(code) ?? null;\n}\n\n// Close button on the selected row's lines cell. Has to clear BOTH\n// expandedEntryId AND entryBeingEdited — if the user clicks Edit\n// (which sets entryBeingEdited) and then clicks Close, leaving\n// entryBeingEdited stale would block reopening: toggleExpanded's\n// edit-mode guard early-returns when entryBeingEdited.id matches the\n// clicked row, so the user could never reopen that entry from the\n// list. Issue surfaced by the CodeRabbit review on PR #1161.\nfunction onCloseDetail(): void {\n expandedEntryId.value = null;\n entryBeingEdited.value = null;\n}\n\nfunction toggleExpanded(entryId: string): void {\n // While the row is in edit mode for itself, ignore clicks on the\n // row chrome (date / kind / memo / lines cells) — the user is\n // actively typing into the form below and a stray cell click\n // shouldn't collapse the panel. Cancel / Submit on the form, or\n // clicking a different row, are the deliberate exits.\n if (entryBeingEdited.value?.id === entryId) return;\n expandedEntryId.value = expandedEntryId.value === entryId ? null : entryId;\n // Switching to a different row (or collapsing) drops any\n // in-progress edit on the prior row.\n entryBeingEdited.value = null;\n}\n\nfunction onKeyToggle(event: KeyboardEvent, entryId: string): void {\n if (event.repeat) return;\n toggleExpanded(entryId);\n}\n\nfunction entryHasTaxIds(entry: JournalEntry): boolean {\n return entry.lines.some((line) => Boolean(line.taxRegistrationId));\n}\n\nfunction sumLines(lines: JournalLine[], pick: (line: JournalLine) => number | undefined): number {\n return lines.reduce((acc, line) => acc + (pick(line) ?? 0), 0);\n}\n\nfunction entryDebitTotal(entry: JournalEntry): number {\n return sumLines(entry.lines, (line) => line.debit);\n}\n\nfunction entryCreditTotal(entry: JournalEntry): number {\n return sumLines(entry.lines, (line) => line.credit);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n const result = await getJournalEntries({\n bookId: props.bookId,\n from: range.value.from || undefined,\n to: range.value.to || undefined,\n accountCode: accountCode.value || undefined,\n });\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n entries.value = [];\n serverVoidedIds.value = [];\n return;\n }\n entries.value = result.data.entries;\n serverVoidedIds.value = result.data.voidedEntryIds;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nconst filteredEntries = computed(() => entries.value);\n\n// Visible-list view that pins the entry currently being edited at\n// the top when a filter change or pubsub-driven refetch would\n// otherwise drop it from `filteredEntries`. Without this, the\n// in-place edit form (which is nested under the row's v-if /\n// v-for) would unmount and silently discard the user's draft when:\n// • the user adjusts the date range or account filter,\n// • a sibling tab / LLM tool voids the entry out-of-band and the\n// SSE pubsub bumps `bookVersion`, refetching this list,\n// • or a sibling tab / LLM tool deletes the underlying book.\n// Pinning the editing entry from the local snapshot (entryBeingEdited)\n// keeps the form mounted across all three. The pinned row sits at\n// the top of the table while editing; on submit / cancel the\n// snapshot clears and the list reverts to filteredEntries.\nconst visibleEntries = computed<JournalEntry[]>(() => {\n const list = filteredEntries.value;\n const editing = entryBeingEdited.value;\n if (editing && !list.some((entry) => entry.id === editing.id)) {\n return [editing, ...list];\n }\n return list;\n});\n\n// Set of original entry ids that have been voided. The server\n// computes this from the *unfiltered* journal (so an account-filtered\n// query — which drops void-marker rows because they have no lines —\n// still strikes out the cancelled original). Source of truth on the\n// server is `voidedIdSet()` in journal.ts.\nconst voidedEntryIds = computed(() => new Set(serverVoidedIds.value));\n\nasync function onVoid(entry: JournalEntry): Promise<void> {\n // Single dialog: the prompt is the confirmation. Cancelling\n // (returning null) cancels the void; entering empty text or a\n // reason proceeds.\n const reason = window.prompt(t(\"pluginAccounting.journalList.voidReason\"));\n if (reason === null) return;\n try {\n const result = await voidEntry({ entryId: entry.id, reason: reason || undefined, bookId: props.bookId });\n if (!result.ok) error.value = result.error;\n } catch (err) {\n error.value = errorMessage(err);\n }\n}\n\n// Reset to current-year window whenever the active book or its\n// fiscal-year end changes. Keeps a custom range from leaking across\n// books and follows a settings-driven shift in fiscalYearEnd.\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\nwatch(() => [props.bookId, props.version, range.value.from, range.value.to, accountCode.value], refresh, { immediate: true });\n\n// Pending preselect: the parent hands us an id via `preselectEntryId`,\n// but the matching entry may not be in `entries` yet (the SSE-driven\n// refetch lands on its own clock). Stash it here, then the\n// [pendingPreselectId, entries] watcher below consumes it once the\n// row actually exists in the list — and clears it so subsequent\n// unrelated refetches (void events, sibling-tab edits) don't\n// re-expand a stale target.\nconst pendingPreselectId = ref<string | null>(null);\n\nwatch(\n () => props.preselectEntryId,\n (incoming) => {\n if (incoming) pendingPreselectId.value = incoming;\n },\n // immediate: true so a late JournalList mount (the View defers our\n // mount until refetchBooks resolves activeBookId) still captures\n // a preselect the parent had already set — without this, a normal\n // watcher misses the \"initial value is the target value\" case.\n { immediate: true },\n);\n\nwatch([pendingPreselectId, entries], async ([targetId, list]) => {\n if (!targetId) return;\n if (!list.some((entry) => entry.id === targetId)) return;\n // Always emit `preselectConsumed` (whether we expand or bail) so\n // the parent can drop its `journalPreselectEntryId` ref. Without\n // this one-shot signal, leaving and returning to the journal tab\n // (v-if remount) replays the immediate prop watcher against the\n // stale value, re-expanding an old row the user has already moved\n // past. Issue raised by the Codex automated review on PR #1158.\n if (entryBeingEdited.value) {\n // Don't overwrite an in-progress edit on another row — the\n // user's draft matters more than the highlight. Drop pending so\n // we don't keep retrying every refetch, and signal consumed so\n // the parent doesn't keep re-handing us the same id.\n pendingPreselectId.value = null;\n emit(\"preselectConsumed\");\n return;\n }\n expandedEntryId.value = targetId;\n await nextTick();\n const row =\n document.querySelector(`[data-testid=\"accounting-journal-row-${targetId}\"]`) ??\n document.querySelector(`[data-testid=\"accounting-journal-row-voided-${targetId}\"]`);\n row?.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n pendingPreselectId.value = null;\n emit(\"preselectConsumed\");\n});\n</script>\n\n<style scoped>\n/* Selection frame for the expanded entry. Borders go on the cells\n (not the <tr>) because border-collapse: collapse — Tailwind's\n default — eats <tr>-level borders/box-shadows. The entry row owns\n top/left/right; the detail-panel row directly below owns\n left/right/bottom, so together they read as one rectangle around\n the selection. Color matches the focus-ring blue used elsewhere\n in this list. */\n.row-selected > td {\n background-color: rgb(239 246 255); /* tailwind blue-50 */\n border-top: 2px solid rgb(59 130 246); /* tailwind blue-500 */\n}\n.row-selected > td:first-child {\n border-left: 2px solid rgb(59 130 246);\n}\n.row-selected > td:last-child {\n border-right: 2px solid rgb(59 130 246);\n}\n.detail-selected > td {\n background-color: rgb(239 246 255);\n border-left: 2px solid rgb(59 130 246);\n border-right: 2px solid rgb(59 130 246);\n border-bottom: 2px solid rgb(59 130 246);\n}\n</style>\n","<template>\n <form class=\"flex flex-col gap-3\" data-testid=\"accounting-opening-form\" @submit.prevent=\"onSubmit\">\n <div class=\"flex items-center justify-between gap-2\">\n <h3 class=\"text-base font-semibold\">{{ t(\"pluginAccounting.openingForm.title\") }}</h3>\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-opening-manage-accounts\"\n @click=\"showAccountsModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.openingForm.explainer\") }}</p>\n <p class=\"text-xs text-blue-600\" data-testid=\"accounting-opening-empty-hint\">{{ t(\"pluginAccounting.openingForm.emptyHint\") }}</p>\n <div v-if=\"existing\" class=\"text-xs text-gray-500\" data-testid=\"accounting-opening-existing\">\n {{ t(\"pluginAccounting.openingForm.setBy\", { date: existing.date }) }}\n <span v-if=\"existing\" class=\"text-amber-600 ml-2\">{{ t(\"pluginAccounting.openingForm.replaceWarning\") }}</span>\n </div>\n <p v-else class=\"text-xs text-gray-400\" data-testid=\"accounting-opening-none\">{{ t(\"pluginAccounting.openingForm.none\") }}</p>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-fit\">\n {{ t(\"pluginAccounting.openingForm.asOfLabel\") }}\n <input v-model=\"asOfDate\" type=\"date\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-opening-asof\" />\n </label>\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"account in bsAccounts\" :key=\"account.code\" class=\"border-b border-gray-100\">\n <td class=\"py-1 px-2\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ account.code }}</span>\n <span>{{ account.name }}</span>\n <span class=\"ml-2 text-xs text-gray-400\">{{ account.type }}</span>\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"rows[account.code].debit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right\"\n :data-testid=\"`accounting-opening-debit-${account.code}`\"\n @input=\"onDebitInput(account.code)\"\n />\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"rows[account.code].credit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right\"\n :data-testid=\"`accounting-opening-credit-${account.code}`\"\n @input=\"onCreditInput(account.code)\"\n />\n </td>\n </tr>\n </tbody>\n </table>\n <div class=\"flex items-center justify-between\">\n <span class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.openingForm.explainer2\") }}</span>\n <span :class=\"balanced ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-opening-balance\">\n {{ balanced ? t(\"pluginAccounting.entryForm.balanced\") : t(\"pluginAccounting.entryForm.imbalance\", { amount: imbalanceText }) }}\n </span>\n </div>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-opening-error\">{{ error }}</p>\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-opening-success\">{{ successMessage }}</p>\n <div class=\"flex justify-end\">\n <button\n type=\"submit\"\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"!balanced || submitting\"\n data-testid=\"accounting-opening-submit\"\n >\n {{ submitting ? t(\"pluginAccounting.entryForm.submitting\") : t(\"pluginAccounting.openingForm.submit\") }}\n </button>\n </div>\n </form>\n <!-- Sibling of the parent <form> on purpose: the modal renders\n its own <form @submit.prevent> for the inline editor, and\n nesting <form>s is invalid HTML that breaks Enter-key submit\n routing in some browsers. Vue 3 multi-root templates let us\n keep the markup flat with no wrapper div. -->\n <AccountsModal v-if=\"showAccountsModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showAccountsModal = false\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { getOpeningBalances, setOpeningBalances, type Account, type JournalEntry, type JournalLine } from \"../api\";\nimport { formatAmount, inputStepFor, localDateString } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport AccountsModal from \"./AccountsModal.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[]; currency: string; version: number }>();\nconst emit = defineEmits<{ submitted: [] }>();\n\nconst showAccountsModal = ref(false);\n\ninterface OpeningRow {\n debit: number | null;\n credit: number | null;\n}\n\nconst asOfDate = ref(localDateString());\nconst rows = ref<Record<string, OpeningRow>>({});\nconst existing = ref<JournalEntry | null>(null);\nconst submitting = ref(false);\nconst error = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\nconst { begin: beginLoad, isCurrent: isCurrentLoad } = useLatestRequest();\n\nconst bsAccounts = computed(() =>\n props.accounts.filter((account) => (account.type === \"asset\" || account.type === \"liability\" || account.type === \"equity\") && account.active !== false),\n);\n\nfunction ensureRows(): void {\n for (const account of bsAccounts.value) {\n if (!rows.value[account.code]) rows.value[account.code] = { debit: null, credit: null };\n }\n}\n\nfunction onDebitInput(code: string): void {\n const row = rows.value[code];\n if (row.debit !== null && row.debit !== 0) row.credit = null;\n}\nfunction onCreditInput(code: string): void {\n const row = rows.value[code];\n if (row.credit !== null && row.credit !== 0) row.debit = null;\n}\n\nconst imbalance = computed<number>(() => {\n // Iterate the live bsAccounts (already active-filtered) rather\n // than rows.value keys so a row for a now-inactive account\n // doesn't tilt `balanced` against what `toApiLines` will\n // actually post.\n let sum = 0;\n for (const account of bsAccounts.value) {\n const row = rows.value[account.code];\n if (!row) continue;\n if (typeof row.debit === \"number\") sum += row.debit;\n if (typeof row.credit === \"number\") sum -= row.credit;\n }\n return sum;\n});\n// An all-empty form is valid: it submits as a zero-line opening\n// marker so the user can unlock the rest of the UI without\n// committing to specific balances on day one.\nconst balanced = computed(() => Math.abs(imbalance.value) <= 0.005);\nconst imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));\nconst step = computed(() => inputStepFor(props.currency));\n\nfunction isPositiveAmount(value: unknown): value is number {\n // Robust against the empty string `v-model.number` produces when\n // the user clears a previously-typed field — without this, the\n // skip condition `value === 0` was false for `\"\"` and the form\n // emitted ghost lines like `{accountCode: \"3000\"}` with no\n // amount on either side.\n return typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\nfunction toApiLines(): JournalLine[] {\n const out: JournalLine[] = [];\n // Iterate the visible bsAccounts list (which already filters\n // out inactive accounts) rather than `rows.value` keys. A row\n // for an account that was active when the user typed amounts\n // and then got deactivated mid-edit would otherwise still post —\n // the row stays in the map even after the v-for stops rendering\n // it, so iterating keys would silently land entries on a\n // soft-deleted account.\n for (const account of bsAccounts.value) {\n const row = rows.value[account.code];\n if (!row) continue;\n const debitOk = isPositiveAmount(row.debit);\n const creditOk = isPositiveAmount(row.credit);\n if (!debitOk && !creditOk) continue;\n const line: JournalLine = { accountCode: account.code };\n if (debitOk) line.debit = row.debit as number;\n if (creditOk) line.credit = row.credit as number;\n out.push(line);\n }\n return out;\n}\n\nfunction freshRows(): Record<string, OpeningRow> {\n const out: Record<string, OpeningRow> = {};\n for (const account of bsAccounts.value) out[account.code] = { debit: null, credit: null };\n return out;\n}\n\nasync function loadExisting(): Promise<void> {\n // Always start from a fresh row map so a book without an\n // opening doesn't inherit the previous book's draft values.\n const token = beginLoad();\n const next = freshRows();\n const result = await getOpeningBalances(props.bookId);\n // Drop the result if the user has switched books since this\n // call started — otherwise stale rows would land on the new\n // book's form.\n if (!isCurrentLoad(token)) return;\n if (!result.ok) {\n existing.value = null;\n rows.value = next;\n return;\n }\n existing.value = result.data.opening;\n if (result.data.opening) {\n asOfDate.value = result.data.opening.date;\n for (const line of result.data.opening.lines) {\n next[line.accountCode] = { debit: line.debit ?? null, credit: line.credit ?? null };\n }\n } else {\n asOfDate.value = localDateString();\n }\n rows.value = next;\n}\n\nasync function onSubmit(): Promise<void> {\n if (submitting.value || !balanced.value) return;\n submitting.value = true;\n error.value = null;\n successMessage.value = null;\n try {\n const result = await setOpeningBalances({ bookId: props.bookId, asOfDate: asOfDate.value, lines: toApiLines() });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n successMessage.value = t(\"pluginAccounting.openingForm.success\");\n emit(\"submitted\");\n } catch (err) {\n error.value = errorMessage(err);\n } finally {\n submitting.value = false;\n }\n}\n\nwatch(\n () => [props.bookId, props.version, props.accounts.length],\n () => {\n ensureRows();\n void loadExisting();\n },\n { immediate: true },\n);\n</script>\n\n<style scoped>\n/* Hide the WebKit / Firefox spin buttons on amount inputs. The\n step attribute still controls validation; this is purely UI.\n Accounting amount fields don't benefit from a spinner — users\n type the number and the up/down arrows just clutter the row. */\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n appearance: textfield;\n}\n</style>\n","<template>\n <form class=\"flex flex-col gap-3\" data-testid=\"accounting-opening-form\" @submit.prevent=\"onSubmit\">\n <div class=\"flex items-center justify-between gap-2\">\n <h3 class=\"text-base font-semibold\">{{ t(\"pluginAccounting.openingForm.title\") }}</h3>\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-opening-manage-accounts\"\n @click=\"showAccountsModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.openingForm.explainer\") }}</p>\n <p class=\"text-xs text-blue-600\" data-testid=\"accounting-opening-empty-hint\">{{ t(\"pluginAccounting.openingForm.emptyHint\") }}</p>\n <div v-if=\"existing\" class=\"text-xs text-gray-500\" data-testid=\"accounting-opening-existing\">\n {{ t(\"pluginAccounting.openingForm.setBy\", { date: existing.date }) }}\n <span v-if=\"existing\" class=\"text-amber-600 ml-2\">{{ t(\"pluginAccounting.openingForm.replaceWarning\") }}</span>\n </div>\n <p v-else class=\"text-xs text-gray-400\" data-testid=\"accounting-opening-none\">{{ t(\"pluginAccounting.openingForm.none\") }}</p>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1 w-fit\">\n {{ t(\"pluginAccounting.openingForm.asOfLabel\") }}\n <input v-model=\"asOfDate\" type=\"date\" required class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-opening-asof\" />\n </label>\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.entryForm.accountLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.debitLabel\") }}</th>\n <th class=\"text-right py-1 px-2 w-32\">{{ t(\"pluginAccounting.entryForm.creditLabel\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"account in bsAccounts\" :key=\"account.code\" class=\"border-b border-gray-100\">\n <td class=\"py-1 px-2\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ account.code }}</span>\n <span>{{ account.name }}</span>\n <span class=\"ml-2 text-xs text-gray-400\">{{ account.type }}</span>\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"rows[account.code].debit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right\"\n :data-testid=\"`accounting-opening-debit-${account.code}`\"\n @input=\"onDebitInput(account.code)\"\n />\n </td>\n <td class=\"py-1 px-2\">\n <input\n v-model.number=\"rows[account.code].credit\"\n type=\"number\"\n :step=\"step\"\n min=\"0\"\n class=\"h-8 px-2 w-full rounded border border-gray-300 text-sm text-right\"\n :data-testid=\"`accounting-opening-credit-${account.code}`\"\n @input=\"onCreditInput(account.code)\"\n />\n </td>\n </tr>\n </tbody>\n </table>\n <div class=\"flex items-center justify-between\">\n <span class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.openingForm.explainer2\") }}</span>\n <span :class=\"balanced ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-opening-balance\">\n {{ balanced ? t(\"pluginAccounting.entryForm.balanced\") : t(\"pluginAccounting.entryForm.imbalance\", { amount: imbalanceText }) }}\n </span>\n </div>\n <p v-if=\"error\" class=\"text-xs text-red-500\" data-testid=\"accounting-opening-error\">{{ error }}</p>\n <p v-if=\"successMessage\" class=\"text-xs text-green-600\" data-testid=\"accounting-opening-success\">{{ successMessage }}</p>\n <div class=\"flex justify-end\">\n <button\n type=\"submit\"\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"!balanced || submitting\"\n data-testid=\"accounting-opening-submit\"\n >\n {{ submitting ? t(\"pluginAccounting.entryForm.submitting\") : t(\"pluginAccounting.openingForm.submit\") }}\n </button>\n </div>\n </form>\n <!-- Sibling of the parent <form> on purpose: the modal renders\n its own <form @submit.prevent> for the inline editor, and\n nesting <form>s is invalid HTML that breaks Enter-key submit\n routing in some browsers. Vue 3 multi-root templates let us\n keep the markup flat with no wrapper div. -->\n <AccountsModal v-if=\"showAccountsModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showAccountsModal = false\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { getOpeningBalances, setOpeningBalances, type Account, type JournalEntry, type JournalLine } from \"../api\";\nimport { formatAmount, inputStepFor, localDateString } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport AccountsModal from \"./AccountsModal.vue\";\nimport { errorMessage } from \"../../shared/errors\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[]; currency: string; version: number }>();\nconst emit = defineEmits<{ submitted: [] }>();\n\nconst showAccountsModal = ref(false);\n\ninterface OpeningRow {\n debit: number | null;\n credit: number | null;\n}\n\nconst asOfDate = ref(localDateString());\nconst rows = ref<Record<string, OpeningRow>>({});\nconst existing = ref<JournalEntry | null>(null);\nconst submitting = ref(false);\nconst error = ref<string | null>(null);\nconst successMessage = ref<string | null>(null);\nconst { begin: beginLoad, isCurrent: isCurrentLoad } = useLatestRequest();\n\nconst bsAccounts = computed(() =>\n props.accounts.filter((account) => (account.type === \"asset\" || account.type === \"liability\" || account.type === \"equity\") && account.active !== false),\n);\n\nfunction ensureRows(): void {\n for (const account of bsAccounts.value) {\n if (!rows.value[account.code]) rows.value[account.code] = { debit: null, credit: null };\n }\n}\n\nfunction onDebitInput(code: string): void {\n const row = rows.value[code];\n if (row.debit !== null && row.debit !== 0) row.credit = null;\n}\nfunction onCreditInput(code: string): void {\n const row = rows.value[code];\n if (row.credit !== null && row.credit !== 0) row.debit = null;\n}\n\nconst imbalance = computed<number>(() => {\n // Iterate the live bsAccounts (already active-filtered) rather\n // than rows.value keys so a row for a now-inactive account\n // doesn't tilt `balanced` against what `toApiLines` will\n // actually post.\n let sum = 0;\n for (const account of bsAccounts.value) {\n const row = rows.value[account.code];\n if (!row) continue;\n if (typeof row.debit === \"number\") sum += row.debit;\n if (typeof row.credit === \"number\") sum -= row.credit;\n }\n return sum;\n});\n// An all-empty form is valid: it submits as a zero-line opening\n// marker so the user can unlock the rest of the UI without\n// committing to specific balances on day one.\nconst balanced = computed(() => Math.abs(imbalance.value) <= 0.005);\nconst imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));\nconst step = computed(() => inputStepFor(props.currency));\n\nfunction isPositiveAmount(value: unknown): value is number {\n // Robust against the empty string `v-model.number` produces when\n // the user clears a previously-typed field — without this, the\n // skip condition `value === 0` was false for `\"\"` and the form\n // emitted ghost lines like `{accountCode: \"3000\"}` with no\n // amount on either side.\n return typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\nfunction toApiLines(): JournalLine[] {\n const out: JournalLine[] = [];\n // Iterate the visible bsAccounts list (which already filters\n // out inactive accounts) rather than `rows.value` keys. A row\n // for an account that was active when the user typed amounts\n // and then got deactivated mid-edit would otherwise still post —\n // the row stays in the map even after the v-for stops rendering\n // it, so iterating keys would silently land entries on a\n // soft-deleted account.\n for (const account of bsAccounts.value) {\n const row = rows.value[account.code];\n if (!row) continue;\n const debitOk = isPositiveAmount(row.debit);\n const creditOk = isPositiveAmount(row.credit);\n if (!debitOk && !creditOk) continue;\n const line: JournalLine = { accountCode: account.code };\n if (debitOk) line.debit = row.debit as number;\n if (creditOk) line.credit = row.credit as number;\n out.push(line);\n }\n return out;\n}\n\nfunction freshRows(): Record<string, OpeningRow> {\n const out: Record<string, OpeningRow> = {};\n for (const account of bsAccounts.value) out[account.code] = { debit: null, credit: null };\n return out;\n}\n\nasync function loadExisting(): Promise<void> {\n // Always start from a fresh row map so a book without an\n // opening doesn't inherit the previous book's draft values.\n const token = beginLoad();\n const next = freshRows();\n const result = await getOpeningBalances(props.bookId);\n // Drop the result if the user has switched books since this\n // call started — otherwise stale rows would land on the new\n // book's form.\n if (!isCurrentLoad(token)) return;\n if (!result.ok) {\n existing.value = null;\n rows.value = next;\n return;\n }\n existing.value = result.data.opening;\n if (result.data.opening) {\n asOfDate.value = result.data.opening.date;\n for (const line of result.data.opening.lines) {\n next[line.accountCode] = { debit: line.debit ?? null, credit: line.credit ?? null };\n }\n } else {\n asOfDate.value = localDateString();\n }\n rows.value = next;\n}\n\nasync function onSubmit(): Promise<void> {\n if (submitting.value || !balanced.value) return;\n submitting.value = true;\n error.value = null;\n successMessage.value = null;\n try {\n const result = await setOpeningBalances({ bookId: props.bookId, asOfDate: asOfDate.value, lines: toApiLines() });\n if (!result.ok) {\n error.value = result.error;\n return;\n }\n successMessage.value = t(\"pluginAccounting.openingForm.success\");\n emit(\"submitted\");\n } catch (err) {\n error.value = errorMessage(err);\n } finally {\n submitting.value = false;\n }\n}\n\nwatch(\n () => [props.bookId, props.version, props.accounts.length],\n () => {\n ensureRows();\n void loadExisting();\n },\n { immediate: true },\n);\n</script>\n\n<style scoped>\n/* Hide the WebKit / Firefox spin buttons on amount inputs. The\n step attribute still controls validation; this is purely UI.\n Accounting amount fields don't benefit from a spinner — users\n type the number and the up/down arrows just clutter the row. */\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\ninput[type=\"number\"] {\n -moz-appearance: textfield;\n appearance: textfield;\n}\n</style>\n","<template>\n <!-- Full-tab chart-of-accounts list. Distinct from AccountsModal\n (called from the entry / opening forms): this one fills the\n canvas, surfaces a single \"Manage accounts\" button at the top,\n and emits `selectAccount` so the parent View can route the\n click into the Ledger tab pre-filtered to that account. -->\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-accounts-list\">\n <div class=\"flex flex-wrap items-center justify-end gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-accounts-manage\"\n @click=\"showManageModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <section v-for=\"group in groups\" :key=\"group.type\" class=\"flex flex-col gap-1\">\n <h4 class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ t(`pluginAccounting.accounts.sectionTitle.${group.type}`) }}</h4>\n <p v-if=\"group.accounts.length === 0\" class=\"text-xs text-gray-400 italic px-1\">{{ t(\"pluginAccounting.accounts.listEmpty\") }}</p>\n <ul v-else class=\"flex flex-col\">\n <li\n v-for=\"account in group.accounts\"\n :key=\"account.code\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: account.code, name: account.name })\"\n class=\"flex items-center gap-3 px-2 py-1.5 border-b border-gray-100 hover:bg-blue-50 cursor-pointer text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 rounded\"\n :data-testid=\"`accounting-account-row-${account.code}`\"\n @click=\"onSelect(account)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, account)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, account)\"\n >\n <span class=\"font-mono text-xs w-16 shrink-0\">{{ account.code }}</span>\n <span class=\"text-sm flex-1 min-w-0 truncate\">{{ account.name }}</span>\n </li>\n </ul>\n </section>\n <AccountsModal v-if=\"showManageModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showManageModal = false\" @changed=\"onAccountsChanged\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport type { Account, AccountType } from \"../api\";\nimport AccountsModal from \"./AccountsModal.vue\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[] }>();\nconst emit = defineEmits<{ selectAccount: [code: string]; changed: [] }>();\n\nconst ACCOUNT_TYPES: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\n\nconst showManageModal = ref(false);\n\ninterface AccountGroup {\n type: AccountType;\n accounts: Account[];\n}\n\nfunction byCode(left: Account, right: Account): number {\n return left.code.localeCompare(right.code);\n}\n\n// Soft-deleted accounts (active === false) are hidden — managing\n// them lives in the Manage Accounts modal, where Reactivate is one\n// click away.\nconst groups = computed<AccountGroup[]>(() =>\n ACCOUNT_TYPES.map((type) => ({\n type,\n accounts: props.accounts\n .filter((account) => account.type === type && account.active !== false)\n .slice()\n .sort(byCode),\n })),\n);\n\nfunction onSelect(account: Account): void {\n emit(\"selectAccount\", account.code);\n}\n\n// Keyboard activation: Enter / Space on a focused row. The\n// `.prevent.self` modifiers in the template stop the default scroll\n// (Space) and ensure we don't fire when the event bubbles up from\n// a focused descendant (currently none, but defensive for future\n// row content).\nfunction onKeyActivate(event: KeyboardEvent, account: Account): void {\n if (event.repeat) return;\n emit(\"selectAccount\", account.code);\n}\n\nfunction onAccountsChanged(): void {\n // Forward to the parent — `bookVersion` already drives the\n // accounts refetch in View.vue, so the list updates without us\n // doing anything extra. Bubble the event in case a future\n // consumer needs it.\n emit(\"changed\");\n}\n</script>\n","<template>\n <!-- Full-tab chart-of-accounts list. Distinct from AccountsModal\n (called from the entry / opening forms): this one fills the\n canvas, surfaces a single \"Manage accounts\" button at the top,\n and emits `selectAccount` so the parent View can route the\n click into the Ledger tab pre-filtered to that account. -->\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-accounts-list\">\n <div class=\"flex flex-wrap items-center justify-end gap-2\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-accounts-manage\"\n @click=\"showManageModal = true\"\n >\n <span class=\"material-icons text-base\">tune</span>\n <span>{{ t(\"pluginAccounting.accounts.manageButton\") }}</span>\n </button>\n </div>\n <section v-for=\"group in groups\" :key=\"group.type\" class=\"flex flex-col gap-1\">\n <h4 class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ t(`pluginAccounting.accounts.sectionTitle.${group.type}`) }}</h4>\n <p v-if=\"group.accounts.length === 0\" class=\"text-xs text-gray-400 italic px-1\">{{ t(\"pluginAccounting.accounts.listEmpty\") }}</p>\n <ul v-else class=\"flex flex-col\">\n <li\n v-for=\"account in group.accounts\"\n :key=\"account.code\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: account.code, name: account.name })\"\n class=\"flex items-center gap-3 px-2 py-1.5 border-b border-gray-100 hover:bg-blue-50 cursor-pointer text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 rounded\"\n :data-testid=\"`accounting-account-row-${account.code}`\"\n @click=\"onSelect(account)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, account)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, account)\"\n >\n <span class=\"font-mono text-xs w-16 shrink-0\">{{ account.code }}</span>\n <span class=\"text-sm flex-1 min-w-0 truncate\">{{ account.name }}</span>\n </li>\n </ul>\n </section>\n <AccountsModal v-if=\"showManageModal\" :book-id=\"bookId\" :accounts=\"accounts\" @close=\"showManageModal = false\" @changed=\"onAccountsChanged\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport type { Account, AccountType } from \"../api\";\nimport AccountsModal from \"./AccountsModal.vue\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ bookId: string; accounts: Account[] }>();\nconst emit = defineEmits<{ selectAccount: [code: string]; changed: [] }>();\n\nconst ACCOUNT_TYPES: readonly AccountType[] = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"];\n\nconst showManageModal = ref(false);\n\ninterface AccountGroup {\n type: AccountType;\n accounts: Account[];\n}\n\nfunction byCode(left: Account, right: Account): number {\n return left.code.localeCompare(right.code);\n}\n\n// Soft-deleted accounts (active === false) are hidden — managing\n// them lives in the Manage Accounts modal, where Reactivate is one\n// click away.\nconst groups = computed<AccountGroup[]>(() =>\n ACCOUNT_TYPES.map((type) => ({\n type,\n accounts: props.accounts\n .filter((account) => account.type === type && account.active !== false)\n .slice()\n .sort(byCode),\n })),\n);\n\nfunction onSelect(account: Account): void {\n emit(\"selectAccount\", account.code);\n}\n\n// Keyboard activation: Enter / Space on a focused row. The\n// `.prevent.self` modifiers in the template stop the default scroll\n// (Space) and ensure we don't fire when the event bubbles up from\n// a focused descendant (currently none, but defensive for future\n// row content).\nfunction onKeyActivate(event: KeyboardEvent, account: Account): void {\n if (event.repeat) return;\n emit(\"selectAccount\", account.code);\n}\n\nfunction onAccountsChanged(): void {\n // Forward to the parent — `bookVersion` already drives the\n // accounts refetch in View.vue, so the list updates without us\n // doing anything extra. Bubble the event in case a future\n // consumer needs it.\n emit(\"changed\");\n}\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-ledger\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.ledger.selectAccount\") }}\n <select v-model=\"accountCode\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-ledger-account\">\n <option value=\"\">{{ DASH }}</option>\n <option v-for=\"account in selectableAccounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </label>\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"ledger\">\n <table class=\"w-full text-sm\" :data-testid=\"showTaxRegistrationColumn ? 'accounting-ledger-table-with-tax-id' : 'accounting-ledger-table'\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.ledger.columns.date\") }}</th>\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.ledger.columns.memo\") }}</th>\n <th v-if=\"showTaxRegistrationColumn\" class=\"text-left py-1 px-2 w-40\">\n {{ t(\"pluginAccounting.ledger.columns.taxRegistrationId\") }}\n </th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.debit\") }}</th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.credit\") }}</th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.balance\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr\n v-for=\"row in ledger.rows\"\n :key=\"`${row.entryId}-${row.date}`\"\n :class=\"row.kind === 'void' || row.kind === 'void-marker' ? 'text-gray-400 line-through' : ''\"\n class=\"border-b border-gray-100\"\n >\n <td class=\"py-1 px-2 whitespace-nowrap\">{{ row.date }}</td>\n <td class=\"py-1 px-2\">\n <span v-if=\"row.memo\">{{ row.memo }}</span>\n </td>\n <td v-if=\"showTaxRegistrationColumn\" class=\"py-1 px-2 font-mono text-xs\">\n <span v-if=\"row.taxRegistrationId\">{{ row.taxRegistrationId }}</span>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <span v-if=\"row.debit\">{{ formatAmount(row.debit) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <span v-if=\"row.credit\">{{ formatAmount(row.credit) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(row.runningBalance) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td :colspan=\"showTaxRegistrationColumn ? 5 : 4\" class=\"py-1 px-2 text-right\">\n {{ t(\"pluginAccounting.ledger.closingBalance\") }}\n </td>\n <td class=\"py-1 px-2 text-right\">{{ formatAmount(ledger.closingBalance) }}</td>\n </tr>\n </tfoot>\n </table>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { getLedger, type Account, type Ledger, type ReportPeriod } from \"../api\";\nimport { formatAmount as formatAmountWithCurrency, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd } from \"../../shared\";\nimport { isTaxAccountCode } from \"./accountNumbering\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{\n bookId: string;\n accounts: Account[];\n currency: string;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n /** Optional account to preselect (Accounts tab → click). Updates\n * via the watcher below — assigning to the local `accountCode`\n * ref keeps the dropdown's v-model authoritative for user edits. */\n preselectAccountCode?: string;\n}>();\n\nconst DASH = \"—\";\nconst accountCode = ref(\"\");\nconst ledger = ref<Ledger | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\n// Default range = current fiscal year. Re-evaluated when bookId or\n// fiscalYearEnd changes (see watcher) so switching books resets to a\n// sensible window rather than carrying the prior book's custom edits.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n return `${account.name} (${account.code})`;\n}\n\n// Hide deactivated accounts from the ledger picker; historical\n// entries on a soft-deleted account are still inspectable via\n// the journal-list filter (which intentionally shows every code\n// so the past stays queryable).\nconst selectableAccounts = computed<Account[]>(() => props.accounts.filter((account) => account.active !== false));\n\n// Surface the T-number column when the active account is in the\n// input-tax band (14xx — e.g. 1400 Input Tax Receivable).\n// Convention-driven so any custom account a user adds in the band\n// participates without an opt-in flag. 24xx (Sales Tax Payable\n// and friends) intentionally doesn't get the column — the\n// counterparty registration ID matters for input-tax-credit\n// eligibility on purchases, not for the seller-side liability.\nconst showTaxRegistrationColumn = computed<boolean>(() => {\n if (!ledger.value) return false;\n return isTaxAccountCode(ledger.value.accountCode);\n});\n\n// Build a ReportPeriod from the current range. Both ends empty = no\n// filter (full history); either end alone gets a sentinel on the\n// other side so the server-side range filter still applies.\nfunction periodFromRange(value: DateRange): ReportPeriod | undefined {\n if (value.from === \"\" && value.to === \"\") return undefined;\n return { kind: \"range\", from: value.from || \"0000-01-01\", to: value.to || \"9999-12-31\" };\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n if (!accountCode.value) {\n ledger.value = null;\n error.value = null;\n loading.value = false;\n return;\n }\n loading.value = true;\n error.value = null;\n try {\n const result = await getLedger(accountCode.value, periodFromRange(range.value), props.bookId);\n // Drop the result if a newer refresh started (bookId or\n // accountCode changed under us) — otherwise a slower earlier\n // request could overwrite the fresh ledger.\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n ledger.value = null;\n return;\n }\n ledger.value = result.data.ledger;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\n// Reset to current-year window AND drop the selected account\n// whenever the active book or its fiscal-year end changes. Without\n// the accountCode reset, switching from book A (cash=1000) to book\n// B (which may not even define 1000) fires a getLedger for a\n// missing code and surfaces an avoidable 404. The range reset\n// follows the same logic — a custom window from book A is\n// meaningless against book B's entries.\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n accountCode.value = \"\";\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\n// Apply parent-supplied preselection (Accounts tab → click). The\n// watcher fires on both initial mount (with `immediate`) and on\n// every prop change so re-clicking the same account from the\n// Accounts tab while already on the Ledger still routes through.\n// Resets the range to the current fiscal year on each preselect so\n// a stale custom window the user left behind on the Ledger doesn't\n// hide the entries the Accounts tab handed off.\nwatch(\n () => props.preselectAccountCode,\n (next) => {\n if (!next) return;\n accountCode.value = next;\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n { immediate: true },\n);\n\nwatch(() => [props.bookId, props.version, accountCode.value, range.value.from, range.value.to], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-ledger\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.ledger.selectAccount\") }}\n <select v-model=\"accountCode\" class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\" data-testid=\"accounting-ledger-account\">\n <option value=\"\">{{ DASH }}</option>\n <option v-for=\"account in selectableAccounts\" :key=\"account.code\" :value=\"account.code\">{{ formatAccountLabel(account) }}</option>\n </select>\n </label>\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"ledger\">\n <table class=\"w-full text-sm\" :data-testid=\"showTaxRegistrationColumn ? 'accounting-ledger-table-with-tax-id' : 'accounting-ledger-table'\">\n <thead>\n <tr class=\"text-xs text-gray-500 border-b border-gray-200\">\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.ledger.columns.date\") }}</th>\n <th class=\"text-left py-1 px-2\">{{ t(\"pluginAccounting.ledger.columns.memo\") }}</th>\n <th v-if=\"showTaxRegistrationColumn\" class=\"text-left py-1 px-2 w-40\">\n {{ t(\"pluginAccounting.ledger.columns.taxRegistrationId\") }}\n </th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.debit\") }}</th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.credit\") }}</th>\n <th class=\"text-right py-1 px-2 w-28\">{{ t(\"pluginAccounting.ledger.columns.balance\") }}</th>\n </tr>\n </thead>\n <tbody>\n <tr\n v-for=\"row in ledger.rows\"\n :key=\"`${row.entryId}-${row.date}`\"\n :class=\"row.kind === 'void' || row.kind === 'void-marker' ? 'text-gray-400 line-through' : ''\"\n class=\"border-b border-gray-100\"\n >\n <td class=\"py-1 px-2 whitespace-nowrap\">{{ row.date }}</td>\n <td class=\"py-1 px-2\">\n <span v-if=\"row.memo\">{{ row.memo }}</span>\n </td>\n <td v-if=\"showTaxRegistrationColumn\" class=\"py-1 px-2 font-mono text-xs\">\n <span v-if=\"row.taxRegistrationId\">{{ row.taxRegistrationId }}</span>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <span v-if=\"row.debit\">{{ formatAmount(row.debit) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right\">\n <span v-if=\"row.credit\">{{ formatAmount(row.credit) }}</span>\n </td>\n <td class=\"py-1 px-2 text-right font-mono\">{{ formatAmount(row.runningBalance) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td :colspan=\"showTaxRegistrationColumn ? 5 : 4\" class=\"py-1 px-2 text-right\">\n {{ t(\"pluginAccounting.ledger.closingBalance\") }}\n </td>\n <td class=\"py-1 px-2 text-right\">{{ formatAmount(ledger.closingBalance) }}</td>\n </tr>\n </tfoot>\n </table>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { getLedger, type Account, type Ledger, type ReportPeriod } from \"../api\";\nimport { formatAmount as formatAmountWithCurrency, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd } from \"../../shared\";\nimport { isTaxAccountCode } from \"./accountNumbering\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{\n bookId: string;\n accounts: Account[];\n currency: string;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n /** Optional account to preselect (Accounts tab → click). Updates\n * via the watcher below — assigning to the local `accountCode`\n * ref keeps the dropdown's v-model authoritative for user edits. */\n preselectAccountCode?: string;\n}>();\n\nconst DASH = \"—\";\nconst accountCode = ref(\"\");\nconst ledger = ref<Ledger | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\n// Default range = current fiscal year. Re-evaluated when bookId or\n// fiscalYearEnd changes (see watcher) so switching books resets to a\n// sensible window rather than carrying the prior book's custom edits.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nfunction formatAccountLabel(account: Account): string {\n // Name first so type-to-search in the <select> matches the\n // human-meaningful word; the code goes in trailing parens.\n return `${account.name} (${account.code})`;\n}\n\n// Hide deactivated accounts from the ledger picker; historical\n// entries on a soft-deleted account are still inspectable via\n// the journal-list filter (which intentionally shows every code\n// so the past stays queryable).\nconst selectableAccounts = computed<Account[]>(() => props.accounts.filter((account) => account.active !== false));\n\n// Surface the T-number column when the active account is in the\n// input-tax band (14xx — e.g. 1400 Input Tax Receivable).\n// Convention-driven so any custom account a user adds in the band\n// participates without an opt-in flag. 24xx (Sales Tax Payable\n// and friends) intentionally doesn't get the column — the\n// counterparty registration ID matters for input-tax-credit\n// eligibility on purchases, not for the seller-side liability.\nconst showTaxRegistrationColumn = computed<boolean>(() => {\n if (!ledger.value) return false;\n return isTaxAccountCode(ledger.value.accountCode);\n});\n\n// Build a ReportPeriod from the current range. Both ends empty = no\n// filter (full history); either end alone gets a sentinel on the\n// other side so the server-side range filter still applies.\nfunction periodFromRange(value: DateRange): ReportPeriod | undefined {\n if (value.from === \"\" && value.to === \"\") return undefined;\n return { kind: \"range\", from: value.from || \"0000-01-01\", to: value.to || \"9999-12-31\" };\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n if (!accountCode.value) {\n ledger.value = null;\n error.value = null;\n loading.value = false;\n return;\n }\n loading.value = true;\n error.value = null;\n try {\n const result = await getLedger(accountCode.value, periodFromRange(range.value), props.bookId);\n // Drop the result if a newer refresh started (bookId or\n // accountCode changed under us) — otherwise a slower earlier\n // request could overwrite the fresh ledger.\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n ledger.value = null;\n return;\n }\n ledger.value = result.data.ledger;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\n// Reset to current-year window AND drop the selected account\n// whenever the active book or its fiscal-year end changes. Without\n// the accountCode reset, switching from book A (cash=1000) to book\n// B (which may not even define 1000) fires a getLedger for a\n// missing code and surfaces an avoidable 404. The range reset\n// follows the same logic — a custom window from book A is\n// meaningless against book B's entries.\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n accountCode.value = \"\";\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\n// Apply parent-supplied preselection (Accounts tab → click). The\n// watcher fires on both initial mount (with `immediate`) and on\n// every prop change so re-clicking the same account from the\n// Accounts tab while already on the Ledger still routes through.\n// Resets the range to the current fiscal year on each preselect so\n// a stale custom window the user left behind on the Ledger doesn't\n// hide the entries the Accounts tab handed off.\nwatch(\n () => props.preselectAccountCode,\n (next) => {\n if (!next) return;\n accountCode.value = next;\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n { immediate: true },\n);\n\nwatch(() => [props.bookId, props.version, accountCode.value, range.value.from, range.value.to], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-balance-sheet\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.balanceSheet.shortcutLabel\") }}\n <select\n :value=\"selectedShortcut\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-bs-shortcut\"\n @change=\"onShortcutChange(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"\" hidden></option>\n <option value=\"thisMonth\">{{ t(\"pluginAccounting.balanceSheet.thisMonth\") }}</option>\n <option value=\"lastMonth\">{{ t(\"pluginAccounting.balanceSheet.lastMonth\") }}</option>\n <option value=\"lastQuarter\">{{ t(\"pluginAccounting.balanceSheet.lastQuarter\") }}</option>\n <option value=\"lastYear\">{{ t(\"pluginAccounting.balanceSheet.lastYear\") }}</option>\n </select>\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.balanceSheet.asOfLabel\") }}\n <input v-model=\"period\" type=\"month\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-bs-period\" />\n </label>\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"balanceSheet\">\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <section v-for=\"section in balanceSheet.sections\" :key=\"section.type\" class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ sectionLabel(section.type) }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in section.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100\"\n :class=\"\n isEarningsRow(row)\n ? 'italic text-gray-600'\n : 'hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400'\n \"\n :tabindex=\"isEarningsRow(row) ? -1 : 0\"\n :role=\"isEarningsRow(row) ? undefined : 'button'\"\n :aria-label=\"isEarningsRow(row) ? undefined : t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"isEarningsRow(row) ? undefined : `accounting-bs-row-${row.accountCode}`\"\n @click=\"onRowClick(row)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row)\"\n >\n <td class=\"py-1 px-1\">\n <span v-if=\"!isEarningsRow(row)\" class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ rowName(row) }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.balance) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(section.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n </div>\n <p :class=\"Math.abs(balanceSheet.imbalance) <= 0.01 ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-bs-imbalance\">\n {{ t(\"pluginAccounting.balanceSheet.imbalance\", { amount: formatAmount(balanceSheet.imbalance) }) }}\n </p>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { getBalanceSheet, type BalanceSheet } from \"../api\";\nimport {\n formatAmount as formatAmountWithCurrency,\n decemberOfPreviousYearString,\n lastMonthOfPreviousQuarterString,\n localMonthString,\n previousMonthString,\n} from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ bookId: string; currency: string; version: number }>();\nconst emit = defineEmits<{ selectAccount: [code: string] }>();\n\nconst period = ref(localMonthString());\nconst balanceSheet = ref<BalanceSheet | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nfunction sectionLabel(type: string): string {\n if (type === \"asset\") return t(\"pluginAccounting.balanceSheet.sections.asset\");\n if (type === \"liability\") return t(\"pluginAccounting.balanceSheet.sections.liability\");\n if (type === \"equity\") return t(\"pluginAccounting.balanceSheet.sections.equity\");\n return type;\n}\n\n// The server adds a synthetic \"Current period earnings\" row to the\n// Equity section so the B/S balances during the period (before\n// closing entries fold income/expense into Retained Earnings).\n// `_currentEarnings` is the sentinel accountCode emitted by the\n// server — see CURRENT_EARNINGS_ACCOUNT_CODE in\n// server/accounting/report.ts.\nconst CURRENT_EARNINGS_ACCOUNT_CODE = \"_currentEarnings\";\n\ninterface BSRow {\n accountCode: string;\n accountName: string;\n balance: number;\n}\n\nfunction isEarningsRow(row: BSRow): boolean {\n return row.accountCode === CURRENT_EARNINGS_ACCOUNT_CODE;\n}\n\nfunction rowName(row: BSRow): string {\n return isEarningsRow(row) ? t(\"pluginAccounting.balanceSheet.currentEarnings\") : row.accountName;\n}\n\n// Earnings row is synthetic (no underlying account on file), so it\n// can't be drilled into. Real-account rows route to the Ledger tab\n// pre-filtered to that account — same pattern as AccountsList.\nfunction onRowClick(row: BSRow): void {\n if (isEarningsRow(row)) return;\n emit(\"selectAccount\", row.accountCode);\n}\n\nfunction onKeyActivate(event: KeyboardEvent, row: BSRow): void {\n if (event.repeat) return;\n if (isEarningsRow(row)) return;\n emit(\"selectAccount\", row.accountCode);\n}\n\n// Mirrors the DateRangePicker pattern: hidden \"\" sentinel for the\n// \"no preset matches\" custom state, otherwise the dropdown shows\n// whichever shortcut produces the current period. Re-evaluates `now`\n// on every read so the labels stay correct across midnight without\n// any cache-invalidation plumbing.\ntype Shortcut = \"thisMonth\" | \"lastMonth\" | \"lastQuarter\" | \"lastYear\";\ntype SelectedShortcut = Shortcut | \"\";\n\nconst selectedShortcut = computed<SelectedShortcut>(() => {\n const { value } = period;\n const now = new Date();\n if (value === localMonthString(now)) return \"thisMonth\";\n if (value === previousMonthString(now)) return \"lastMonth\";\n if (value === lastMonthOfPreviousQuarterString(now)) return \"lastQuarter\";\n if (value === decemberOfPreviousYearString(now)) return \"lastYear\";\n return \"\";\n});\n\nfunction onShortcutChange(raw: string): void {\n const now = new Date();\n if (raw === \"thisMonth\") period.value = localMonthString(now);\n else if (raw === \"lastMonth\") period.value = previousMonthString(now);\n else if (raw === \"lastQuarter\") period.value = lastMonthOfPreviousQuarterString(now);\n else if (raw === \"lastYear\") period.value = decemberOfPreviousYearString(now);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n const result = await getBalanceSheet({ kind: \"month\", period: period.value }, props.bookId);\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n balanceSheet.value = null;\n return;\n }\n balanceSheet.value = result.data.balanceSheet;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nwatch(() => [props.bookId, props.version, period.value], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-balance-sheet\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.balanceSheet.shortcutLabel\") }}\n <select\n :value=\"selectedShortcut\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-bs-shortcut\"\n @change=\"onShortcutChange(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"\" hidden></option>\n <option value=\"thisMonth\">{{ t(\"pluginAccounting.balanceSheet.thisMonth\") }}</option>\n <option value=\"lastMonth\">{{ t(\"pluginAccounting.balanceSheet.lastMonth\") }}</option>\n <option value=\"lastQuarter\">{{ t(\"pluginAccounting.balanceSheet.lastQuarter\") }}</option>\n <option value=\"lastYear\">{{ t(\"pluginAccounting.balanceSheet.lastYear\") }}</option>\n </select>\n </label>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.balanceSheet.asOfLabel\") }}\n <input v-model=\"period\" type=\"month\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-bs-period\" />\n </label>\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"balanceSheet\">\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <section v-for=\"section in balanceSheet.sections\" :key=\"section.type\" class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ sectionLabel(section.type) }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in section.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100\"\n :class=\"\n isEarningsRow(row)\n ? 'italic text-gray-600'\n : 'hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400'\n \"\n :tabindex=\"isEarningsRow(row) ? -1 : 0\"\n :role=\"isEarningsRow(row) ? undefined : 'button'\"\n :aria-label=\"isEarningsRow(row) ? undefined : t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"isEarningsRow(row) ? undefined : `accounting-bs-row-${row.accountCode}`\"\n @click=\"onRowClick(row)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row)\"\n >\n <td class=\"py-1 px-1\">\n <span v-if=\"!isEarningsRow(row)\" class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ rowName(row) }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.balance) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(section.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n </div>\n <p :class=\"Math.abs(balanceSheet.imbalance) <= 0.01 ? 'text-green-600' : 'text-red-500'\" class=\"text-xs\" data-testid=\"accounting-bs-imbalance\">\n {{ t(\"pluginAccounting.balanceSheet.imbalance\", { amount: formatAmount(balanceSheet.imbalance) }) }}\n </p>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { getBalanceSheet, type BalanceSheet } from \"../api\";\nimport {\n formatAmount as formatAmountWithCurrency,\n decemberOfPreviousYearString,\n lastMonthOfPreviousQuarterString,\n localMonthString,\n previousMonthString,\n} from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ bookId: string; currency: string; version: number }>();\nconst emit = defineEmits<{ selectAccount: [code: string] }>();\n\nconst period = ref(localMonthString());\nconst balanceSheet = ref<BalanceSheet | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nfunction sectionLabel(type: string): string {\n if (type === \"asset\") return t(\"pluginAccounting.balanceSheet.sections.asset\");\n if (type === \"liability\") return t(\"pluginAccounting.balanceSheet.sections.liability\");\n if (type === \"equity\") return t(\"pluginAccounting.balanceSheet.sections.equity\");\n return type;\n}\n\n// The server adds a synthetic \"Current period earnings\" row to the\n// Equity section so the B/S balances during the period (before\n// closing entries fold income/expense into Retained Earnings).\n// `_currentEarnings` is the sentinel accountCode emitted by the\n// server — see CURRENT_EARNINGS_ACCOUNT_CODE in\n// server/accounting/report.ts.\nconst CURRENT_EARNINGS_ACCOUNT_CODE = \"_currentEarnings\";\n\ninterface BSRow {\n accountCode: string;\n accountName: string;\n balance: number;\n}\n\nfunction isEarningsRow(row: BSRow): boolean {\n return row.accountCode === CURRENT_EARNINGS_ACCOUNT_CODE;\n}\n\nfunction rowName(row: BSRow): string {\n return isEarningsRow(row) ? t(\"pluginAccounting.balanceSheet.currentEarnings\") : row.accountName;\n}\n\n// Earnings row is synthetic (no underlying account on file), so it\n// can't be drilled into. Real-account rows route to the Ledger tab\n// pre-filtered to that account — same pattern as AccountsList.\nfunction onRowClick(row: BSRow): void {\n if (isEarningsRow(row)) return;\n emit(\"selectAccount\", row.accountCode);\n}\n\nfunction onKeyActivate(event: KeyboardEvent, row: BSRow): void {\n if (event.repeat) return;\n if (isEarningsRow(row)) return;\n emit(\"selectAccount\", row.accountCode);\n}\n\n// Mirrors the DateRangePicker pattern: hidden \"\" sentinel for the\n// \"no preset matches\" custom state, otherwise the dropdown shows\n// whichever shortcut produces the current period. Re-evaluates `now`\n// on every read so the labels stay correct across midnight without\n// any cache-invalidation plumbing.\ntype Shortcut = \"thisMonth\" | \"lastMonth\" | \"lastQuarter\" | \"lastYear\";\ntype SelectedShortcut = Shortcut | \"\";\n\nconst selectedShortcut = computed<SelectedShortcut>(() => {\n const { value } = period;\n const now = new Date();\n if (value === localMonthString(now)) return \"thisMonth\";\n if (value === previousMonthString(now)) return \"lastMonth\";\n if (value === lastMonthOfPreviousQuarterString(now)) return \"lastQuarter\";\n if (value === decemberOfPreviousYearString(now)) return \"lastYear\";\n return \"\";\n});\n\nfunction onShortcutChange(raw: string): void {\n const now = new Date();\n if (raw === \"thisMonth\") period.value = localMonthString(now);\n else if (raw === \"lastMonth\") period.value = previousMonthString(now);\n else if (raw === \"lastQuarter\") period.value = lastMonthOfPreviousQuarterString(now);\n else if (raw === \"lastYear\") period.value = decemberOfPreviousYearString(now);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n const result = await getBalanceSheet({ kind: \"month\", period: period.value }, props.bookId);\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n balanceSheet.value = null;\n return;\n }\n balanceSheet.value = result.data.balanceSheet;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nwatch(() => [props.bookId, props.version, period.value], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-profit-loss\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"profitLoss\">\n <section class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ t(\"pluginAccounting.profitLoss.income\") }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in profitLoss.income.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"`accounting-pl-row-${row.accountCode}`\"\n @click=\"onRowClick(row.accountCode)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n >\n <td class=\"py-1 px-1\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ row.accountName }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.amount) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(profitLoss.income.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n <section class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ t(\"pluginAccounting.profitLoss.expense\") }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in profitLoss.expense.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"`accounting-pl-row-${row.accountCode}`\"\n @click=\"onRowClick(row.accountCode)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n >\n <td class=\"py-1 px-1\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ row.accountName }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.amount) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(profitLoss.expense.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n <div class=\"flex justify-end items-center gap-2 text-sm font-semibold\" data-testid=\"accounting-pl-net\">\n <span>{{ t(\"pluginAccounting.profitLoss.netIncome\") }}</span>\n <span :class=\"profitLoss.netIncome >= 0 ? 'text-green-600' : 'text-red-500'\">{{ formatAmount(profitLoss.netIncome) }}</span>\n </div>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { getProfitLoss, type ProfitLoss } from \"../api\";\nimport { formatAmount as formatAmountWithCurrency, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{\n bookId: string;\n currency: string;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n}>();\n\nconst emit = defineEmits<{ selectAccount: [code: string] }>();\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\nfunction onRowClick(code: string): void {\n emit(\"selectAccount\", code);\n}\n\nfunction onKeyActivate(event: KeyboardEvent, code: string): void {\n if (event.repeat) return;\n emit(\"selectAccount\", code);\n}\n\n// Default = current fiscal year. Reset by the bookId/fiscalYearEnd\n// watcher below so switching books or changing the FY-end in\n// settings drops a stale custom range from the prior book.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\nconst profitLoss = ref<ProfitLoss | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n // P&L always sends a range. Empty-side gets a sentinel so \"All\"\n // (both empty) means \"every entry\" rather than an empty window.\n const fromBound = range.value.from || \"0000-01-01\";\n const toBound = range.value.to || \"9999-12-31\";\n const result = await getProfitLoss({ kind: \"range\", from: fromBound, to: toBound }, props.bookId);\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n profitLoss.value = null;\n return;\n }\n profitLoss.value = result.data.profitLoss;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\nwatch(() => [props.bookId, props.version, range.value.from, range.value.to], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-3\" data-testid=\"accounting-profit-loss\">\n <div class=\"flex flex-wrap items-end gap-3\">\n <DateRangePicker v-model=\"range\" :fiscal-year-end=\"resolvedFiscalYearEnd\" :opening-date=\"openingDate\" />\n <button class=\"h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\" @click=\"refresh\">\n <span class=\"material-icons text-base align-middle\">refresh</span>\n </button>\n </div>\n <p v-if=\"loading\" class=\"text-xs text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"error\" class=\"text-xs text-red-500\">{{ t(\"pluginAccounting.common.error\", { error }) }}</p>\n <template v-else-if=\"profitLoss\">\n <section class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ t(\"pluginAccounting.profitLoss.income\") }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in profitLoss.income.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"`accounting-pl-row-${row.accountCode}`\"\n @click=\"onRowClick(row.accountCode)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n >\n <td class=\"py-1 px-1\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ row.accountName }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.amount) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(profitLoss.income.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n <section class=\"border border-gray-200 rounded p-3\">\n <h4 class=\"text-sm font-semibold mb-2\">{{ t(\"pluginAccounting.profitLoss.expense\") }}</h4>\n <table class=\"w-full text-sm\">\n <tbody>\n <tr\n v-for=\"row in profitLoss.expense.rows\"\n :key=\"row.accountCode\"\n class=\"border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400\"\n tabindex=\"0\"\n role=\"button\"\n :aria-label=\"t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })\"\n :data-testid=\"`accounting-pl-row-${row.accountCode}`\"\n @click=\"onRowClick(row.accountCode)\"\n @keydown.enter.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n @keydown.space.prevent.self=\"onKeyActivate($event, row.accountCode)\"\n >\n <td class=\"py-1 px-1\">\n <span class=\"font-mono text-[10px] text-gray-400 mr-2\">{{ row.accountCode }}</span\n >{{ row.accountName }}\n </td>\n <td class=\"py-1 px-1 text-right font-mono\">{{ formatAmount(row.amount) }}</td>\n </tr>\n </tbody>\n <tfoot>\n <tr class=\"font-semibold border-t border-gray-300\">\n <td class=\"py-1 px-1\">{{ t(\"pluginAccounting.balanceSheet.total\") }}</td>\n <td class=\"py-1 px-1 text-right\">{{ formatAmount(profitLoss.expense.total) }}</td>\n </tr>\n </tfoot>\n </table>\n </section>\n <div class=\"flex justify-end items-center gap-2 text-sm font-semibold\" data-testid=\"accounting-pl-net\">\n <span>{{ t(\"pluginAccounting.profitLoss.netIncome\") }}</span>\n <span :class=\"profitLoss.netIncome >= 0 ? 'text-green-600' : 'text-red-500'\">{{ formatAmount(profitLoss.netIncome) }}</span>\n </div>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { getProfitLoss, type ProfitLoss } from \"../api\";\nimport { formatAmount as formatAmountWithCurrency, currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd } from \"../../shared\";\nimport { useLatestRequest } from \"./useLatestRequest\";\nimport DateRangePicker from \"./DateRangePicker.vue\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{\n bookId: string;\n currency: string;\n version: number;\n fiscalYearEnd?: FiscalYearEnd;\n /** Opening-balance date for the active book — drives the \"Lifetime\"\n * shortcut in the date picker (from = openingDate, to = today).\n * When absent, the picker hides Lifetime; \"All\" still works. */\n openingDate?: string;\n}>();\n\nconst emit = defineEmits<{ selectAccount: [code: string] }>();\n\nconst resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));\n\nfunction onRowClick(code: string): void {\n emit(\"selectAccount\", code);\n}\n\nfunction onKeyActivate(event: KeyboardEvent, code: string): void {\n if (event.repeat) return;\n emit(\"selectAccount\", code);\n}\n\n// Default = current fiscal year. Reset by the bookId/fiscalYearEnd\n// watcher below so switching books or changing the FY-end in\n// settings drops a stale custom range from the prior book.\nconst range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));\nconst profitLoss = ref<ProfitLoss | null>(null);\nconst loading = ref(false);\nconst error = ref<string | null>(null);\nconst { begin: beginRequest, isCurrent } = useLatestRequest();\n\nfunction formatAmount(value: number): string {\n return formatAmountWithCurrency(value, props.currency);\n}\n\nasync function refresh(): Promise<void> {\n const token = beginRequest();\n loading.value = true;\n error.value = null;\n try {\n // P&L always sends a range. Empty-side gets a sentinel so \"All\"\n // (both empty) means \"every entry\" rather than an empty window.\n const fromBound = range.value.from || \"0000-01-01\";\n const toBound = range.value.to || \"9999-12-31\";\n const result = await getProfitLoss({ kind: \"range\", from: fromBound, to: toBound }, props.bookId);\n if (!isCurrent(token)) return;\n if (!result.ok) {\n error.value = result.error;\n profitLoss.value = null;\n return;\n }\n profitLoss.value = result.data.profitLoss;\n } finally {\n if (isCurrent(token)) loading.value = false;\n }\n}\n\nwatch(\n () => [props.bookId, resolvedFiscalYearEnd.value],\n () => {\n range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);\n },\n);\n\nwatch(() => [props.bookId, props.version, range.value.from, range.value.to], refresh, { immediate: true });\n</script>\n","<template>\n <div class=\"flex flex-col gap-4\" data-testid=\"accounting-settings\">\n <section class=\"border border-gray-200 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"pluginAccounting.settings.bookInfo\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.bookInfoExplain\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.nameLabel\") }}\n <input\n v-model=\"selectedName\"\n type=\"text\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-name\"\n :disabled=\"updating\"\n maxlength=\"200\"\n />\n </label>\n <dl class=\"text-xs text-gray-700 grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1\">\n <dt class=\"text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.currencyLabel\") }}</dt>\n <dd>{{ currency }}</dd>\n </dl>\n <label class=\"text-sm flex flex-col gap-1 mt-1\">\n {{ t(\"pluginAccounting.bookSwitcher.countryLabel\") }}\n <select\n v-model=\"selectedCountry\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-country\"\n :disabled=\"updating\"\n >\n <option value=\"\">{{ t(\"pluginAccounting.settings.countryUnset\") }}</option>\n <option v-for=\"opt in countryOptions\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <label class=\"text-sm flex flex-col gap-1 mt-1\">\n {{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndLabel\") }}\n <select\n v-model=\"selectedFiscalYearEnd\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-fiscal-year-end\"\n :disabled=\"updating\"\n >\n <option v-for=\"opt in fiscalYearEndOptions\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.fiscalYearEndExplain\") }}</p>\n <p v-if=\"updateOk\" class=\"text-xs text-green-600\" data-testid=\"accounting-settings-update-ok\">{{ updateOk }}</p>\n <p v-if=\"updateError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-update-error\">{{ updateError }}</p>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"updating || !hasPendingChanges\"\n data-testid=\"accounting-settings-save\"\n @click=\"onSaveBookInfo\"\n >\n {{ updating ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.saveChanges\") }}\n </button>\n </div>\n </section>\n <section class=\"border border-gray-200 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"pluginAccounting.settings.rebuild\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.rebuildExplain\") }}</p>\n <p v-if=\"rebuildOk\" class=\"text-xs text-green-600\" data-testid=\"accounting-settings-rebuild-ok\">{{ rebuildOk }}</p>\n <p v-if=\"rebuildError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-rebuild-error\">{{ rebuildError }}</p>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"rebuilding\"\n data-testid=\"accounting-settings-rebuild\"\n @click=\"onRebuild\"\n >\n {{ rebuilding ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.rebuild\") }}\n </button>\n </div>\n </section>\n <div v-if=\"!showAdvanced\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-settings-advanced\"\n @click=\"showAdvanced = true\"\n >\n <span class=\"material-icons text-base\">expand_more</span>\n <span>{{ t(\"pluginAccounting.settings.advanced\") }}</span>\n </button>\n </div>\n <section v-if=\"showAdvanced\" class=\"border border-red-300 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold text-red-700\">{{ t(\"pluginAccounting.settings.deleteBook\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.deleteBookExplain\") }}</p>\n <p v-if=\"deleteError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-delete-error\">{{ deleteError }}</p>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.settings.deleteBookConfirm\", { bookName: bookName }) }}\n <input v-model=\"confirmName\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-settings-delete-confirm\" />\n </label>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-red-600 hover:bg-red-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"confirmName !== bookName || deleting\"\n data-testid=\"accounting-settings-delete\"\n @click=\"onDelete\"\n >\n {{ deleting ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.deleteBookButton\") }}\n </button>\n </div>\n </section>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { deleteBook, rebuildSnapshots, updateBook } from \"../api\";\nimport {\n SUPPORTED_COUNTRY_CODES,\n isSupportedCountryCode,\n localizedCountryName,\n type SupportedCountryCode,\n DEFAULT_FISCAL_YEAR_END,\n FISCAL_YEAR_ENDS,\n resolveFiscalYearEnd,\n type FiscalYearEnd,\n} from \"../../shared\";\n\nconst { t, locale } = useAccountingI18n();\n\nconst props = defineProps<{\n bookId: string;\n bookName: string;\n currency: string;\n country?: SupportedCountryCode;\n fiscalYearEnd?: FiscalYearEnd;\n}>();\nconst emit = defineEmits<{ deleted: [bookName: string]; \"books-changed\": [] }>();\n\nconst rebuilding = ref(false);\nconst rebuildOk = ref<string | null>(null);\nconst rebuildError = ref<string | null>(null);\nconst deleting = ref(false);\nconst deleteError = ref<string | null>(null);\nconst confirmName = ref(\"\");\nconst updating = ref(false);\nconst updateOk = ref<string | null>(null);\nconst updateError = ref<string | null>(null);\nconst showAdvanced = ref(false);\nconst selectedName = ref<string>(props.bookName);\nconst selectedCountry = ref<string>(props.country ?? \"\");\n// Resolved at the boundary so the dropdown always shows a concrete\n// value — books without a `fiscalYearEnd` field on disk land here as\n// the default Q4 (matches the back-compat read policy).\nconst selectedFiscalYearEnd = ref<FiscalYearEnd>(props.fiscalYearEnd ?? DEFAULT_FISCAL_YEAR_END);\n\ninterface CountryOption {\n code: string;\n label: string;\n}\n\nconst countryOptions = computed<CountryOption[]>(() =>\n SUPPORTED_COUNTRY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCountryName(code, locale.value)}`,\n })),\n);\n\ninterface FiscalYearEndOption {\n value: FiscalYearEnd;\n label: string;\n}\n\nconst fiscalYearEndOptions = computed<FiscalYearEndOption[]>(() =>\n FISCAL_YEAR_ENDS.map((value) => ({\n value,\n label: t(`pluginAccounting.bookSwitcher.fiscalYearEnd${value}`),\n })),\n);\n\nconst hasPendingChanges = computed<boolean>(() => {\n // Compare against the trimmed value so a no-op edit (typing then\n // backspacing back to the original) doesn't keep the Save button\n // hot. Server-side validateUpdateBookInput rejects empty / whitespace\n // names with a 400 — the disabled binding below mirrors that contract\n // so the button can't fire a doomed request.\n const nameChanged = selectedName.value.trim() !== props.bookName;\n const nameValid = selectedName.value.trim().length > 0;\n const countryChanged = selectedCountry.value !== (props.country ?? \"\");\n const fiscalChanged = selectedFiscalYearEnd.value !== resolveFiscalYearEnd(props.fiscalYearEnd);\n return nameValid && (nameChanged || countryChanged || fiscalChanged);\n});\n\nasync function onRebuild(): Promise<void> {\n rebuilding.value = true;\n rebuildOk.value = null;\n rebuildError.value = null;\n try {\n const result = await rebuildSnapshots(props.bookId);\n if (!result.ok) {\n rebuildError.value = result.error;\n return;\n }\n rebuildOk.value = t(\"pluginAccounting.settings.rebuildOk\", { count: result.data.rebuilt.length });\n } finally {\n rebuilding.value = false;\n }\n}\n\nasync function onSaveBookInfo(): Promise<void> {\n if (updating.value) return;\n updating.value = true;\n updateOk.value = null;\n updateError.value = null;\n try {\n // The select v-model is a plain `string` (HTML form value); narrow\n // it back to the union before handing it to the API helper. The\n // empty string is the sentinel that clears the country server-side.\n const rawCountry = selectedCountry.value;\n const country: SupportedCountryCode | \"\" = rawCountry === \"\" || isSupportedCountryCode(rawCountry) ? rawCountry : \"\";\n const result = await updateBook({\n bookId: props.bookId,\n name: selectedName.value.trim(),\n country,\n fiscalYearEnd: selectedFiscalYearEnd.value,\n });\n if (!result.ok) {\n updateError.value = result.error;\n return;\n }\n updateOk.value = t(\"pluginAccounting.settings.updateOk\");\n emit(\"books-changed\");\n } finally {\n updating.value = false;\n }\n}\n\nasync function onDelete(): Promise<void> {\n if (deleting.value) return;\n deleting.value = true;\n deleteError.value = null;\n try {\n const result = await deleteBook(props.bookId);\n if (!result.ok) {\n deleteError.value = result.error;\n return;\n }\n emit(\"deleted\", props.bookName);\n emit(\"books-changed\");\n } finally {\n deleting.value = false;\n }\n}\n\n// Reset feedback / confirmation AND the dropdown selection when the\n// user navigates between books while this tab is open. Without the\n// `selectedCountry` reset, switching from book A (country=JP) to\n// book B (also country=JP) leaves a previously-typed unsaved value\n// staged on B — a save would then misattribute the edit.\nwatch(\n () => props.bookId,\n () => {\n rebuildOk.value = null;\n rebuildError.value = null;\n deleteError.value = null;\n confirmName.value = \"\";\n updateOk.value = null;\n updateError.value = null;\n selectedName.value = props.bookName;\n selectedCountry.value = props.country ?? \"\";\n selectedFiscalYearEnd.value = props.fiscalYearEnd ?? DEFAULT_FISCAL_YEAR_END;\n showAdvanced.value = false;\n },\n);\n\n// Follow external bookName updates — e.g. an LLM-driven updateBook in\n// another tab, or pubsub-driven refetch. Without this, an out-of-band\n// rename leaves a stale draft staged in the input.\nwatch(\n () => props.bookName,\n (next) => {\n selectedName.value = next;\n },\n);\n\nwatch(\n () => props.country,\n (next) => {\n selectedCountry.value = next ?? \"\";\n },\n);\n\nwatch(\n () => props.fiscalYearEnd,\n (next) => {\n selectedFiscalYearEnd.value = next ?? DEFAULT_FISCAL_YEAR_END;\n },\n);\n</script>\n","<template>\n <div class=\"flex flex-col gap-4\" data-testid=\"accounting-settings\">\n <section class=\"border border-gray-200 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"pluginAccounting.settings.bookInfo\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.bookInfoExplain\") }}</p>\n <label class=\"text-sm flex flex-col gap-1\">\n {{ t(\"pluginAccounting.bookSwitcher.nameLabel\") }}\n <input\n v-model=\"selectedName\"\n type=\"text\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-name\"\n :disabled=\"updating\"\n maxlength=\"200\"\n />\n </label>\n <dl class=\"text-xs text-gray-700 grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1\">\n <dt class=\"text-gray-500\">{{ t(\"pluginAccounting.bookSwitcher.currencyLabel\") }}</dt>\n <dd>{{ currency }}</dd>\n </dl>\n <label class=\"text-sm flex flex-col gap-1 mt-1\">\n {{ t(\"pluginAccounting.bookSwitcher.countryLabel\") }}\n <select\n v-model=\"selectedCountry\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-country\"\n :disabled=\"updating\"\n >\n <option value=\"\">{{ t(\"pluginAccounting.settings.countryUnset\") }}</option>\n <option v-for=\"opt in countryOptions\" :key=\"opt.code\" :value=\"opt.code\">{{ opt.label }}</option>\n </select>\n </label>\n <label class=\"text-sm flex flex-col gap-1 mt-1\">\n {{ t(\"pluginAccounting.bookSwitcher.fiscalYearEndLabel\") }}\n <select\n v-model=\"selectedFiscalYearEnd\"\n class=\"h-8 px-2 rounded border border-gray-300 text-sm bg-white\"\n data-testid=\"accounting-settings-fiscal-year-end\"\n :disabled=\"updating\"\n >\n <option v-for=\"opt in fiscalYearEndOptions\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n </label>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.fiscalYearEndExplain\") }}</p>\n <p v-if=\"updateOk\" class=\"text-xs text-green-600\" data-testid=\"accounting-settings-update-ok\">{{ updateOk }}</p>\n <p v-if=\"updateError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-update-error\">{{ updateError }}</p>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"updating || !hasPendingChanges\"\n data-testid=\"accounting-settings-save\"\n @click=\"onSaveBookInfo\"\n >\n {{ updating ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.saveChanges\") }}\n </button>\n </div>\n </section>\n <section class=\"border border-gray-200 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"pluginAccounting.settings.rebuild\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.rebuildExplain\") }}</p>\n <p v-if=\"rebuildOk\" class=\"text-xs text-green-600\" data-testid=\"accounting-settings-rebuild-ok\">{{ rebuildOk }}</p>\n <p v-if=\"rebuildError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-rebuild-error\">{{ rebuildError }}</p>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"rebuilding\"\n data-testid=\"accounting-settings-rebuild\"\n @click=\"onRebuild\"\n >\n {{ rebuilding ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.rebuild\") }}\n </button>\n </div>\n </section>\n <div v-if=\"!showAdvanced\">\n <button\n type=\"button\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50\"\n data-testid=\"accounting-settings-advanced\"\n @click=\"showAdvanced = true\"\n >\n <span class=\"material-icons text-base\">expand_more</span>\n <span>{{ t(\"pluginAccounting.settings.advanced\") }}</span>\n </button>\n </div>\n <section v-if=\"showAdvanced\" class=\"border border-red-300 rounded p-3 flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold text-red-700\">{{ t(\"pluginAccounting.settings.deleteBook\") }}</h4>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.settings.deleteBookExplain\") }}</p>\n <p v-if=\"deleteError\" class=\"text-xs text-red-500\" data-testid=\"accounting-settings-delete-error\">{{ deleteError }}</p>\n <label class=\"text-xs text-gray-500 flex flex-col gap-1\">\n {{ t(\"pluginAccounting.settings.deleteBookConfirm\", { bookName: bookName }) }}\n <input v-model=\"confirmName\" class=\"h-8 px-2 rounded border border-gray-300 text-sm\" data-testid=\"accounting-settings-delete-confirm\" />\n </label>\n <div>\n <button\n class=\"h-8 px-3 rounded bg-red-600 hover:bg-red-700 text-white text-sm disabled:opacity-50\"\n :disabled=\"confirmName !== bookName || deleting\"\n data-testid=\"accounting-settings-delete\"\n @click=\"onDelete\"\n >\n {{ deleting ? t(\"pluginAccounting.common.loading\") : t(\"pluginAccounting.settings.deleteBookButton\") }}\n </button>\n </div>\n </section>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"../lang\";\nimport { deleteBook, rebuildSnapshots, updateBook } from \"../api\";\nimport {\n SUPPORTED_COUNTRY_CODES,\n isSupportedCountryCode,\n localizedCountryName,\n type SupportedCountryCode,\n DEFAULT_FISCAL_YEAR_END,\n FISCAL_YEAR_ENDS,\n resolveFiscalYearEnd,\n type FiscalYearEnd,\n} from \"../../shared\";\n\nconst { t, locale } = useAccountingI18n();\n\nconst props = defineProps<{\n bookId: string;\n bookName: string;\n currency: string;\n country?: SupportedCountryCode;\n fiscalYearEnd?: FiscalYearEnd;\n}>();\nconst emit = defineEmits<{ deleted: [bookName: string]; \"books-changed\": [] }>();\n\nconst rebuilding = ref(false);\nconst rebuildOk = ref<string | null>(null);\nconst rebuildError = ref<string | null>(null);\nconst deleting = ref(false);\nconst deleteError = ref<string | null>(null);\nconst confirmName = ref(\"\");\nconst updating = ref(false);\nconst updateOk = ref<string | null>(null);\nconst updateError = ref<string | null>(null);\nconst showAdvanced = ref(false);\nconst selectedName = ref<string>(props.bookName);\nconst selectedCountry = ref<string>(props.country ?? \"\");\n// Resolved at the boundary so the dropdown always shows a concrete\n// value — books without a `fiscalYearEnd` field on disk land here as\n// the default Q4 (matches the back-compat read policy).\nconst selectedFiscalYearEnd = ref<FiscalYearEnd>(props.fiscalYearEnd ?? DEFAULT_FISCAL_YEAR_END);\n\ninterface CountryOption {\n code: string;\n label: string;\n}\n\nconst countryOptions = computed<CountryOption[]>(() =>\n SUPPORTED_COUNTRY_CODES.map((code) => ({\n code,\n label: `${code} — ${localizedCountryName(code, locale.value)}`,\n })),\n);\n\ninterface FiscalYearEndOption {\n value: FiscalYearEnd;\n label: string;\n}\n\nconst fiscalYearEndOptions = computed<FiscalYearEndOption[]>(() =>\n FISCAL_YEAR_ENDS.map((value) => ({\n value,\n label: t(`pluginAccounting.bookSwitcher.fiscalYearEnd${value}`),\n })),\n);\n\nconst hasPendingChanges = computed<boolean>(() => {\n // Compare against the trimmed value so a no-op edit (typing then\n // backspacing back to the original) doesn't keep the Save button\n // hot. Server-side validateUpdateBookInput rejects empty / whitespace\n // names with a 400 — the disabled binding below mirrors that contract\n // so the button can't fire a doomed request.\n const nameChanged = selectedName.value.trim() !== props.bookName;\n const nameValid = selectedName.value.trim().length > 0;\n const countryChanged = selectedCountry.value !== (props.country ?? \"\");\n const fiscalChanged = selectedFiscalYearEnd.value !== resolveFiscalYearEnd(props.fiscalYearEnd);\n return nameValid && (nameChanged || countryChanged || fiscalChanged);\n});\n\nasync function onRebuild(): Promise<void> {\n rebuilding.value = true;\n rebuildOk.value = null;\n rebuildError.value = null;\n try {\n const result = await rebuildSnapshots(props.bookId);\n if (!result.ok) {\n rebuildError.value = result.error;\n return;\n }\n rebuildOk.value = t(\"pluginAccounting.settings.rebuildOk\", { count: result.data.rebuilt.length });\n } finally {\n rebuilding.value = false;\n }\n}\n\nasync function onSaveBookInfo(): Promise<void> {\n if (updating.value) return;\n updating.value = true;\n updateOk.value = null;\n updateError.value = null;\n try {\n // The select v-model is a plain `string` (HTML form value); narrow\n // it back to the union before handing it to the API helper. The\n // empty string is the sentinel that clears the country server-side.\n const rawCountry = selectedCountry.value;\n const country: SupportedCountryCode | \"\" = rawCountry === \"\" || isSupportedCountryCode(rawCountry) ? rawCountry : \"\";\n const result = await updateBook({\n bookId: props.bookId,\n name: selectedName.value.trim(),\n country,\n fiscalYearEnd: selectedFiscalYearEnd.value,\n });\n if (!result.ok) {\n updateError.value = result.error;\n return;\n }\n updateOk.value = t(\"pluginAccounting.settings.updateOk\");\n emit(\"books-changed\");\n } finally {\n updating.value = false;\n }\n}\n\nasync function onDelete(): Promise<void> {\n if (deleting.value) return;\n deleting.value = true;\n deleteError.value = null;\n try {\n const result = await deleteBook(props.bookId);\n if (!result.ok) {\n deleteError.value = result.error;\n return;\n }\n emit(\"deleted\", props.bookName);\n emit(\"books-changed\");\n } finally {\n deleting.value = false;\n }\n}\n\n// Reset feedback / confirmation AND the dropdown selection when the\n// user navigates between books while this tab is open. Without the\n// `selectedCountry` reset, switching from book A (country=JP) to\n// book B (also country=JP) leaves a previously-typed unsaved value\n// staged on B — a save would then misattribute the edit.\nwatch(\n () => props.bookId,\n () => {\n rebuildOk.value = null;\n rebuildError.value = null;\n deleteError.value = null;\n confirmName.value = \"\";\n updateOk.value = null;\n updateError.value = null;\n selectedName.value = props.bookName;\n selectedCountry.value = props.country ?? \"\";\n selectedFiscalYearEnd.value = props.fiscalYearEnd ?? DEFAULT_FISCAL_YEAR_END;\n showAdvanced.value = false;\n },\n);\n\n// Follow external bookName updates — e.g. an LLM-driven updateBook in\n// another tab, or pubsub-driven refetch. Without this, an out-of-band\n// rename leaves a stale draft staged in the input.\nwatch(\n () => props.bookName,\n (next) => {\n selectedName.value = next;\n },\n);\n\nwatch(\n () => props.country,\n (next) => {\n selectedCountry.value = next ?? \"\";\n },\n);\n\nwatch(\n () => props.fiscalYearEnd,\n (next) => {\n selectedFiscalYearEnd.value = next ?? DEFAULT_FISCAL_YEAR_END;\n },\n);\n</script>\n","// Subscribe to per-book accounting events.\n//\n// Returns a `version` ref that bumps every time the server publishes a\n// change for the given bookId — addEntries, voidEntry,\n// setOpeningBalances, upsertAccount, snapshot rebuild completion. View\n// components watch `version` to drive `refetch` calls.\n//\n// `bookId` is reactive: switching the active book in BookSwitcher\n// flips it; the composable unsubscribes from the old channel and\n// subscribes to the new one.\n//\n// `onPayload` is an optional fine-grained hook for callers that want to\n// inspect the event kind (e.g. show a \"rebuilding…\" indicator on\n// `kind: \"snapshots-rebuilding\"`).\n//\n// The raw pub/sub transport is host-injected via `hostSubscribe`\n// (see hostContext.ts) — the channel NAMES come from this package's\n// own `./shared` so publisher and subscriber stay in lockstep.\n\nimport { ref, watch, onUnmounted, type Ref } from \"vue\";\nimport { bookChannel, ACCOUNTING_BOOKS_CHANNEL, type BookChannelPayload } from \"../shared\";\nimport { hostSubscribe } from \"./hostContext\";\n\nexport interface UseAccountingChannelReturn {\n /** Bumps on every per-book event for the current bookId. Resets to\n * 0 when bookId changes. */\n version: Ref<number>;\n}\n\nexport function useAccountingChannel(bookId: Ref<string | null>, onPayload?: (payload: BookChannelPayload) => void): UseAccountingChannelReturn {\n const version = ref(0);\n let unsubscribe: (() => void) | null = null;\n\n function bind(nextBookId: string | null): void {\n unsubscribe?.();\n unsubscribe = null;\n version.value = 0;\n if (!nextBookId) return;\n unsubscribe = hostSubscribe(bookChannel(nextBookId), (data) => {\n const event = data as BookChannelPayload;\n version.value += 1;\n onPayload?.(event);\n });\n }\n\n watch(bookId, bind, { immediate: true });\n onUnmounted(() => {\n unsubscribe?.();\n unsubscribe = null;\n });\n return { version };\n}\n\n/** Subscribe to \"the list of books changed\" events. Use in\n * BookSwitcher.vue to refetch the dropdown contents when a sibling\n * tab adds / deletes a book. */\nexport function useAccountingBooksChannel(onChange: () => void): void {\n const unsubscribe = hostSubscribe(ACCOUNTING_BOOKS_CHANNEL, onChange);\n onUnmounted(() => unsubscribe());\n}\n","<template>\n <!-- Full <AccountingApp> mounted via the openBook tool result.\n Talks to /api/accounting directly for browse / form ops; only\n the entry gate (this mount) runs through the LLM. Pub/sub\n refetches keep multi-tab / sibling-window views in sync. -->\n <div class=\"h-full bg-white flex flex-col\" data-testid=\"accounting-app\">\n <NewBookForm v-if=\"showFirstRunForm\" first-run full-page @created=\"onFirstBookCreated\" />\n <template v-else>\n <header class=\"flex items-center justify-between gap-2 px-3 py-2 border-b border-gray-100 shrink-0\">\n <div class=\"flex items-center gap-2 min-w-0\">\n <span class=\"material-icons text-gray-600\">account_balance</span>\n <h2 class=\"text-lg font-semibold text-gray-800\">{{ t(\"pluginAccounting.title\") }}</h2>\n </div>\n <BookSwitcher\n v-if=\"initialLoadDone\"\n :model-value=\"activeBookId ?? ''\"\n :books=\"books\"\n @update:model-value=\"onBookSelected\"\n @books-changed=\"refetchBooks\"\n @book-created=\"onBookCreated\"\n />\n </header>\n <nav class=\"flex items-center gap-0.5 px-3 py-1.5 border-b border-gray-100 shrink-0 overflow-x-auto\" data-testid=\"accounting-tabs\">\n <button\n v-for=\"tab in visibleTabs\"\n :key=\"tab.key\"\n :class=\"[\n 'h-8 px-2.5 flex items-center gap-1 rounded text-sm whitespace-nowrap',\n deletedNoticeName !== null\n ? 'text-gray-400 cursor-not-allowed'\n : currentTab === tab.key\n ? 'bg-blue-50 text-blue-600 font-medium'\n : 'text-gray-600 hover:bg-gray-50',\n ]\"\n :data-testid=\"`accounting-tab-${tab.key}`\"\n :disabled=\"deletedNoticeName !== null\"\n @click=\"currentTab = tab.key\"\n >\n <span class=\"material-icons text-base\">{{ tab.icon }}</span>\n <span>{{ t(tab.labelKey) }}</span>\n </button>\n </nav>\n <main class=\"flex-1 overflow-auto p-4\">\n <div\n v-if=\"deletedNoticeName !== null\"\n class=\"text-center text-sm text-gray-600 flex flex-col gap-2 items-center justify-center h-full\"\n data-testid=\"accounting-deleted-notice\"\n >\n <span class=\"material-icons text-gray-400\" style=\"font-size: 48px\">delete_outline</span>\n <p class=\"font-medium\" data-testid=\"accounting-deleted-notice-title\">\n {{ t(\"pluginAccounting.deletedNotice.title\", { bookName: deletedNoticeName }) }}\n </p>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.deletedNotice.body\") }}</p>\n </div>\n <p v-else-if=\"loadingBooks && !initialLoadDone\" class=\"text-sm text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"bookLoadError\" class=\"text-sm text-red-500\" data-testid=\"accounting-load-error\">\n {{ t(\"pluginAccounting.common.error\", { error: bookLoadError }) }}\n </p>\n <p v-else-if=\"!activeBookId\" class=\"text-sm text-gray-500\" data-testid=\"accounting-no-book\">{{ t(\"pluginAccounting.noBook\") }}</p>\n <template v-else-if=\"activeBookId\">\n <JournalList\n v-if=\"currentTab === 'journal'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :country=\"activeCountry\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n :preselect-entry-id=\"journalPreselectEntryId\"\n @edit-opening=\"currentTab = 'opening'\"\n @preselect-consumed=\"journalPreselectEntryId = undefined\"\n />\n <OpeningBalancesForm\n v-else-if=\"currentTab === 'opening'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n @submitted=\"onEntrySubmitted\"\n />\n <AccountsList v-else-if=\"currentTab === 'accounts'\" :book-id=\"activeBookId\" :accounts=\"accounts\" @select-account=\"onAccountSelected\" />\n <Ledger\n v-else-if=\"currentTab === 'ledger'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n :preselect-account-code=\"ledgerPreselectAccountCode\"\n />\n <BalanceSheet\n v-else-if=\"currentTab === 'balanceSheet'\"\n :book-id=\"activeBookId\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n @select-account=\"onAccountSelected\"\n />\n <ProfitLoss\n v-else-if=\"currentTab === 'profitLoss'\"\n :book-id=\"activeBookId\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n @select-account=\"onAccountSelected\"\n />\n <BookSettings\n v-else-if=\"currentTab === 'settings'\"\n :book-id=\"activeBookId\"\n :book-name=\"activeBookName\"\n :currency=\"activeCurrency\"\n :country=\"activeCountry\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n @deleted=\"onBookDeleted\"\n @books-changed=\"refetchBooks\"\n />\n </template>\n </main>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"./lang\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport BookSwitcher from \"./components/BookSwitcher.vue\";\nimport NewBookForm from \"./components/NewBookForm.vue\";\nimport JournalList from \"./components/JournalList.vue\";\nimport OpeningBalancesForm from \"./components/OpeningBalancesForm.vue\";\nimport AccountsList from \"./components/AccountsList.vue\";\nimport Ledger from \"./components/Ledger.vue\";\nimport BalanceSheet from \"./components/BalanceSheet.vue\";\nimport ProfitLoss from \"./components/ProfitLoss.vue\";\nimport BookSettings from \"./components/BookSettings.vue\";\nimport { getOpeningBalances, getAccounts, getBooks, type Account, type BookSummary } from \"./api\";\nimport { ACCOUNTING_ACTIONS } from \"../shared\";\nimport { useAccountingChannel, useAccountingBooksChannel } from \"./useAccountingChannel\";\nimport { errorMessage } from \"../shared/errors\";\n\nconst { t } = useAccountingI18n();\n\ninterface AccountingAppPayload {\n kind?: string;\n bookId?: string;\n initialTab?: string;\n /** Dispatch verb stamped onto every accounting tool-result envelope\n * (server/api/routes/accounting.ts dispatch()). We read it here to\n * pick the canvas tab + journal preselect for each PREVIEW action. */\n action?: string;\n /** Present on `addEntries` envelopes — the freshly-built journal\n * entries returned by the service. Each carries a server-stamped\n * `id` we use to highlight the row in JournalList. */\n entries?: { id?: string }[];\n /** Present on `voidEntry` envelopes — the kind=\"void-marker\" row\n * posted alongside the reversing entry. We surface this row (not\n * the reverseEntry) because the marker is the visual \"this entry\n * was voided here\" indicator the user is looking for. */\n markerEntry?: { id?: string };\n}\n\nconst props = defineProps<{ selectedResult?: ToolResultComplete<AccountingAppPayload, AccountingAppPayload> }>();\n\nconst TAB_KEYS = [\"journal\", \"opening\", \"accounts\", \"ledger\", \"balanceSheet\", \"profitLoss\", \"settings\"] as const;\ntype TabKey = (typeof TAB_KEYS)[number];\n\ninterface TabDef {\n key: TabKey;\n icon: string;\n labelKey: string;\n}\n\nconst TABS: readonly TabDef[] = [\n { key: \"journal\", icon: \"list\", labelKey: \"pluginAccounting.tabs.journal\" },\n { key: \"opening\", icon: \"play_arrow\", labelKey: \"pluginAccounting.tabs.opening\" },\n { key: \"accounts\", icon: \"list_alt\", labelKey: \"pluginAccounting.tabs.accounts\" },\n { key: \"ledger\", icon: \"menu_book\", labelKey: \"pluginAccounting.tabs.ledger\" },\n { key: \"balanceSheet\", icon: \"balance\", labelKey: \"pluginAccounting.tabs.balanceSheet\" },\n { key: \"profitLoss\", icon: \"trending_up\", labelKey: \"pluginAccounting.tabs.profitLoss\" },\n { key: \"settings\", icon: \"settings\", labelKey: \"pluginAccounting.tabs.settings\" },\n];\n\nfunction isTabKey(value: string | undefined): value is TabKey {\n return typeof value === \"string\" && (TAB_KEYS as readonly string[]).includes(value);\n}\n\nconst initialPayload = computed<AccountingAppPayload>(() => props.selectedResult?.data ?? props.selectedResult?.jsonData ?? {});\nconst initialTab = computed<TabKey>(() => (isTabKey(initialPayload.value.initialTab) ? initialPayload.value.initialTab : \"journal\"));\n\nconst currentTab = ref<TabKey>(initialTab.value);\nconst books = ref<BookSummary[]>([]);\nconst activeBookId = ref<string | null>(null);\nconst accounts = ref<Account[]>([]);\nconst loadingBooks = ref(true);\n// Sticky once the first books fetch lands. Lets the BookSwitcher stay\n// mounted across subsequent refetches (delete, create, pubsub-driven)\n// so the user sees the dropdown smoothly update its selection rather\n// than having the whole component flash in and out via `v-if`.\nconst initialLoadDone = ref(false);\n// First-run flow: when the user opens the app on a fresh\n// workspace (zero books), we render NewBookForm in full-page\n// mode in place of the regular chrome (header + tabs + main),\n// so the user MUST pick a name + currency before proceeding —\n// no popup, no dismiss. Distinct from the modal opened via\n// BookSwitcher's \"+ New book\" sentinel option, which reuses the\n// same component but with the overlay layout.\nconst showFirstRunForm = ref(false);\nconst firstRunHandled = ref(false);\n// Distinct from \"books is empty\" so we don't show the \"+ New\n// book\" CTA when the real problem is a transport / server failure\n// fetching the list.\nconst bookLoadError = ref<string | null>(null);\n// Tracks whether the active book has an opening entry on file.\n// `null` = unknown / loading; the gate only activates on an\n// explicit `false` so we don't disable tabs during the cold load\n// while the first getOpeningBalances request is still in flight.\nconst hasOpening = ref<boolean | null>(null);\n// Date of the active book's opening entry, plumbed down to the\n// DateRangePicker via the children so \"All\" can resolve to\n// (openingDate → today). `undefined` while loading / for books\n// without an opening on file (the opening gate prevents any tab\n// that would care from being shown in that state).\nconst activeOpeningDate = ref<string | undefined>(undefined);\n// Special \"you just deleted this book\" UI state. When set to a\n// non-null book name, the entire tab strip + main content are\n// replaced by an explicit \"<book> has been deleted — pick another\n// from the dropdown\" panel. Cleared the moment the user picks a\n// book from the BookSwitcher (or creates a new one). The View does\n// NOT auto-route to books[0] because that hides the fact that the\n// previously-active book is gone — issue #1126 (1) calls this\n// experience \"very confusing\".\nconst deletedNoticeName = ref<string | null>(null);\n\nconst activeBook = computed(() => books.value.find((book) => book.id === activeBookId.value) ?? null);\nconst activeBookName = computed(() => activeBook.value?.name ?? \"\");\nconst activeCurrency = computed(() => activeBook.value?.currency ?? \"USD\");\nconst activeCountry = computed(() => activeBook.value?.country);\nconst activeFiscalYearEnd = computed(() => activeBook.value?.fiscalYearEnd);\n\n// Single sync signal: every mutating service function publishes on\n// the accounting book channel after its write, so the sender's own\n// SSE round-trip drives the table/report refetch. No parallel\n// localVersion bump — it only ever fired the same watchers a second\n// time in the same tick.\nconst { version: bookVersion } = useAccountingChannel(activeBookId);\nuseAccountingBooksChannel(() => void refetchBooks());\n\nfunction pickInitialBookId(): string | null {\n // Priority: explicit `initialPayload.bookId` (carried in the\n // tool-result envelope by openBook / createBook / addEntries / …) →\n // first book in the list → null (empty workspace). The candidate\n // is validated against the live book list so a stale id from a\n // deleted book doesn't poison the View.\n if (books.value.length === 0) return null;\n const requested = initialPayload.value.bookId;\n if (requested && books.value.some((book) => book.id === requested)) return requested;\n return books.value[0].id;\n}\n\nasync function refetchBooks(): Promise<void> {\n loadingBooks.value = true;\n bookLoadError.value = null;\n // Capture the current active book BEFORE the fetch so we can\n // surface its name in the deleted-notice panel if the fetch\n // reveals it's gone. Without this snapshot, an SSE-driven refetch\n // racing ahead of the local deleteBook HTTP response would resolve\n // with `activeBook` already pointing at a now-stale entry.\n const previousActive = activeBook.value;\n try {\n const result = await getBooks();\n if (!result.ok) {\n // Surface load failures as a distinct error state so the user\n // doesn't see \"No books yet\" (and the auto-open modal) when\n // the real cause is a transport / server problem.\n bookLoadError.value = result.error;\n return;\n }\n books.value = result.data.books;\n // Sticky-true once a successful fetch lands. Setting it here (in\n // the success branch) rather than in `finally` means a first-load\n // transport / 5xx failure leaves BookSwitcher hidden — the user\n // sees only the `accounting-load-error` message rather than an\n // empty dropdown with a live \"+ New book\" path that has nothing\n // to fall back on.\n initialLoadDone.value = true;\n // While the deleted-notice panel is already up, leave activeBookId\n // alone — the user has to pick the next book themselves via\n // the BookSwitcher (and onBookSelected then clears the notice).\n // Otherwise pickInitialBookId would silently re-select books[0]\n // and undo the entire deletion-state UX.\n if (deletedNoticeName.value === null) {\n const stillExists = activeBookId.value !== null && books.value.some((book) => book.id === activeBookId.value);\n if (!stillExists) {\n // The active book just disappeared from the server's list.\n // Race-source possibilities, all converging here:\n // • local deleteBook → publishBooksChanged → SSE arrives\n // before the HTTP response handler can call onBookDeleted;\n // • a sibling tab / LLM tool deleted the book out-of-band.\n // In all cases the user needs to know what happened — show\n // the deleted-notice panel keyed off the previously-active\n // book's name, rather than silently snapping to books[0].\n // Falls back to the previous pickInitialBookId behaviour only\n // when there was no active book to lose (cold start).\n if (previousActive) {\n activeBookId.value = null;\n deletedNoticeName.value = previousActive.name;\n } else {\n activeBookId.value = pickInitialBookId();\n }\n }\n }\n // Auto-open the New Book modal exactly once on first arrival\n // when the workspace is empty. After that, the user can still\n // open it manually via the \"+ New book\" button.\n if (!firstRunHandled.value && books.value.length === 0) {\n firstRunHandled.value = true;\n showFirstRunForm.value = true;\n }\n } catch (err) {\n bookLoadError.value = errorMessage(err);\n } finally {\n loadingBooks.value = false;\n }\n}\n\nasync function onFirstBookCreated(book: BookSummary): Promise<void> {\n showFirstRunForm.value = false;\n await refetchBooks();\n activeBookId.value = book.id;\n}\n\n// Optimistically insert the new book and set the selection\n// BEFORE the refetch round-trip. Two reasons this beats the\n// previous await-refetch-then-select shape:\n// 1. The pubsub handler `useAccountingBooksChannel` fires its\n// own concurrent `refetchBooks` the instant the server\n// publishes books-changed. With await-then-select, that\n// concurrent refetch's stillExists guard reads the OLD\n// activeBookId (we haven't updated it yet) and — because\n// OLD is still in the books list — leaves the selection\n// pointing at OLD. Our update lands AFTER, but BookSwitcher\n// remounts under `v-if=\"!loadingBooks\"` mid-flight, so the\n// user sees the dropdown stick on OLD.\n// 2. With activeBookId already set to NEW and books pre-\n// populated to include NEW, every concurrent refetch's\n// stillExists check passes for NEW and leaves the selection\n// alone — order-independent by construction.\nasync function onBookCreated(book: BookSummary): Promise<void> {\n if (!books.value.some((existing) => existing.id === book.id)) {\n books.value = [...books.value, book];\n }\n activeBookId.value = book.id;\n // Creating a new book is also the \"exit\" out of the deleted-notice\n // panel — the user explicitly chose the new book, so re-enable the\n // tab strip and let the opening-gate watcher route them to Opening.\n deletedNoticeName.value = null;\n // currentTab may be on \"settings\" (the user opened the create\n // modal from there) — reset to journal so the openingGateActive\n // watcher's \"if (currentTab.value === 'opening') return\" gate\n // doesn't strand the user on settings while the gate is active.\n currentTab.value = \"journal\";\n await refetchBooks();\n}\n\nasync function refetchAccounts(): Promise<void> {\n if (!activeBookId.value) {\n accounts.value = [];\n return;\n }\n const result = await getAccounts(activeBookId.value);\n if (!result.ok) return;\n accounts.value = result.data.accounts;\n}\n\nasync function refetchOpening(): Promise<void> {\n if (!activeBookId.value) {\n hasOpening.value = null;\n activeOpeningDate.value = undefined;\n return;\n }\n const result = await getOpeningBalances(activeBookId.value);\n if (!result.ok) return;\n hasOpening.value = result.data.opening !== null;\n activeOpeningDate.value = result.data.opening?.date;\n}\n\n// A book without an opening on file is in \"gated\" mode: the user\n// must save an opening (empty is fine — see OpeningBalancesForm)\n// before journal / report tabs unlock. Settings stays accessible\n// so the user can delete the book if they don't want to proceed.\nconst openingGateActive = computed(() => activeBookId.value !== null && hasOpening.value === false);\n\n// Gated → only Opening + Settings render in the strip. Ungated →\n// Opening hides itself; users reach the form via the Edit button\n// on the active opening row in the journal, which transiently\n// switches `currentTab` to \"opening\" (kept visible while there).\nconst visibleTabs = computed<readonly TabDef[]>(() => {\n if (openingGateActive.value) return TABS.filter((tab) => tab.key === \"opening\" || tab.key === \"settings\");\n return TABS.filter((tab) => tab.key !== \"opening\" || currentTab.value === \"opening\");\n});\n\nfunction onBookSelected(bookId: string): void {\n activeBookId.value = bookId;\n // Picking a book from the dropdown is the explicit \"I'm done\n // looking at the deleted notice\" exit. Clear it so the tab strip\n // re-enables for the freshly selected book.\n deletedNoticeName.value = null;\n}\n\n// Entry id to surface in JournalList after an `addEntries` tool\n// result lands — the LLM just posted a journal entry and we want\n// the user's eye on the new row. Multi-entry batches highlight the\n// LAST entry only (matches the \"you ended up here\" intent of a\n// scroll-to-cursor).\nconst journalPreselectEntryId = ref<string | undefined>(undefined);\n\n// Account preselected by the Accounts tab → click handoff. Cleared\n// once the user picks a different account from the Ledger's own\n// dropdown so a stale preselection doesn't override later edits.\nconst ledgerPreselectAccountCode = ref<string | undefined>(undefined);\n\nfunction onAccountSelected(code: string): void {\n // Force the ref to a fresh value even when the user clicks the\n // same account a second time — the Ledger's `watch(preselect…)`\n // ignores no-op updates, so we'd otherwise leave the user on a\n // stale Ledger state if they navigated away and clicked back.\n ledgerPreselectAccountCode.value = undefined;\n Promise.resolve().then(() => {\n ledgerPreselectAccountCode.value = code;\n });\n currentTab.value = \"ledger\";\n}\n\nfunction onEntrySubmitted(): void {\n // After saving an opening, switch to the journal so the user\n // immediately sees the unlocked tabs. The server-side\n // publishBookChange triggers the bookVersion watcher over SSE,\n // which refetches hasOpening, so the gate auto-lifts shortly after\n // the tab switch — no manual unlock needed here. Normal entries\n // are now posted from the inline form inside JournalList; that\n // form drives its own dismissal and the journal repaints in\n // place.\n if (currentTab.value === \"opening\") {\n currentTab.value = \"journal\";\n }\n}\n\nasync function onBookDeleted(deletedName: string): Promise<void> {\n // Reset the tab BEFORE awaiting so a fast delete-then-create\n // can't race: if the new book's gate engages while we're still\n // awaiting refetchBooks, the gate watcher needs to see a\n // non-\"settings\" currentTab to route the user to Opening.\n currentTab.value = \"journal\";\n // Drop the active selection so refetchBooks doesn't auto-pick\n // books[0] — the user should see the deleted-notice panel and\n // explicitly switch via the BookSwitcher rather than be silently\n // moved to a different book (issue #1126).\n activeBookId.value = null;\n deletedNoticeName.value = deletedName;\n await refetchBooks();\n}\n\n// Refetch the chart of accounts whenever the active book changes\n// or any pub/sub / child action bumps bookVersion (e.g. an\n// upsertAccount from the Manage Accounts modal, or an LLM-driven\n// upsert in another tab). The list is small JSON; the cost of\n// over-fetching on entry / void / opening events is negligible\n// against the staleness bug it removes.\nwatch(\n () => [activeBookId.value, bookVersion.value],\n () => {\n if (activeBookId.value) void refetchAccounts();\n },\n { immediate: true },\n);\n\n// Drop any leftover Accounts → Ledger preselection when the active\n// book changes. Without this, picking account \"1000\" in book A's\n// Accounts tab and then switching to book B would carry the hint\n// across, so book B's Ledger would auto-select \"1000\" (which may\n// be an unrelated account in B's chart, or absent entirely).\nwatch(activeBookId, () => {\n ledgerPreselectAccountCode.value = undefined;\n});\n\n// Stash a target bookId that we want to land on but haven't been\n// able to apply yet (book not in `books` at the moment the\n// tool-result fired). Cleared as soon as the books list catches up.\nconst pendingTargetBookId = ref<string | null>(null);\n\nfunction applyTargetBookId(target: string): void {\n if (books.value.some((book) => book.id === target)) {\n activeBookId.value = target;\n pendingTargetBookId.value = null;\n return;\n }\n pendingTargetBookId.value = target;\n}\n\n// When the selected tool-result changes (user clicks a different\n// preview card in the sidebar), follow the new result's bookId so\n// the canvas lands on the book that action just touched. Skipped\n// when the new result has no bookId (silent reads / actions that\n// don't carry one). When the target isn't in `books` yet — common\n// race after a fresh `createBook → openBook(bookId)` handoff where\n// the result envelope arrives before refetchBooks completes — the\n// id is stashed and applied by the books watcher below as soon as\n// the list catches up.\nwatch(\n () => initialPayload.value.bookId,\n (next) => {\n if (!next) return;\n applyTargetBookId(next);\n },\n);\n\n// Drains the pending target once `books` includes it (typically\n// after a pub/sub-driven refetch resolves the createBook write).\n// No-op when nothing is pending or the target is still missing.\nwatch(books, () => {\n const pending = pendingTargetBookId.value;\n if (pending) applyTargetBookId(pending);\n});\n\n// Map a PREVIEW action to the canvas tab the user should land on.\n// Honours an explicit `initialTab` from the envelope (the LLM's\n// stated intent) over the action-default below — only `openBook`\n// currently ships initialTab, but the override is plugin-wide.\n//\n// The `balanceSheet` default for openBook / createBook /\n// setOpeningBalances assumes the book has an opening on file. For a\n// fresh book without one, the existing `openingGateActive` watcher\n// redirects to \"opening\" — we don't try to short-circuit that here\n// because hasOpening hasn't necessarily resolved when this runs.\nfunction pickTabForAction(payload: AccountingAppPayload): TabKey | null {\n if (isTabKey(payload.initialTab)) return payload.initialTab;\n switch (payload.action) {\n case ACCOUNTING_ACTIONS.addEntries:\n case ACCOUNTING_ACTIONS.voidEntry:\n return \"journal\";\n case ACCOUNTING_ACTIONS.upsertAccount:\n return \"accounts\";\n case ACCOUNTING_ACTIONS.updateBook:\n return \"settings\";\n case ACCOUNTING_ACTIONS.openBook:\n case ACCOUNTING_ACTIONS.createBook:\n case ACCOUNTING_ACTIONS.setOpeningBalances:\n return \"balanceSheet\";\n default:\n return null;\n }\n}\n\n// For tool results that should auto-expand a row in JournalList,\n// derive the entry id from the action's payload. addEntries picks\n// the LAST entry in the batch (\"you ended up here\" cursor); voidEntry\n// picks the void-MARKER (the visual \"voided here\" indicator), not\n// the reversing entry.\nfunction pickJournalPreselectId(payload: AccountingAppPayload): string | undefined {\n if (payload.action === ACCOUNTING_ACTIONS.addEntries) {\n const entries = Array.isArray(payload.entries) ? payload.entries : [];\n return entries[entries.length - 1]?.id;\n }\n if (payload.action === ACCOUNTING_ACTIONS.voidEntry) {\n return payload.markerEntry?.id;\n }\n return undefined;\n}\n\n// Drive canvas tab + journal preselect from the active tool-result\n// envelope. The route handler stamps `data: { action, bookId, … }`\n// onto every PREVIEW action's response (server/api/routes/\n// accounting.ts dispatch + PREVIEW_ACTIONS). `immediate: true` so a\n// cold open with the result already selected (e.g., reload after\n// the LLM dispatched) routes to the right surface too.\n//\n// Preselect is *always* assigned (not `if (preselect)`) so a\n// subsequent non-addEntries/voidEntry tool result clears any stale\n// id left over from a prior addEntries the user has already seen —\n// otherwise the next JournalList remount would replay it. The child\n// also emits `preselectConsumed` after expanding for the same\n// reason.\nwatch(\n () => initialPayload.value,\n (payload) => {\n const targetTab = pickTabForAction(payload);\n if (targetTab) currentTab.value = targetTab;\n journalPreselectEntryId.value = pickJournalPreselectId(payload);\n },\n { immediate: true },\n);\n\n// Drop the journal preselect on a real book SWITCH — leftover ids\n// from the prior book don't exist in the new one. The cold-load\n// transition (null → bookId) doesn't qualify: refetchBooks resolves\n// activeBookId asynchronously and would otherwise clobber a\n// preselect the addEntries watcher just set on initial mount.\nwatch(activeBookId, (_next, prev) => {\n if (!prev) return;\n journalPreselectEntryId.value = undefined;\n});\n\n// Refetch the opening status whenever the active book changes or\n// any pub/sub / child action bumps bookVersion (e.g. an opening\n// got saved or voided). Clears hasOpening when the book goes null\n// so a stale \"true\" doesn't carry over between books.\nwatch(\n () => [activeBookId.value, bookVersion.value],\n () => void refetchOpening(),\n { immediate: true },\n);\n\n// Force-route to the Opening tab whenever the gate engages.\n// Other tabs are hidden from the strip while gated, but this\n// watcher handles the programmatic case where currentTab still\n// points at a now-hidden tab (book switch, initial mount with a\n// no-opening book, LLM-supplied initialTab pointing at a gated\n// tab, or fresh-book creation right after deleting from the\n// settings tab) — without it, `<main>` would render nothing or\n// the user would be stranded on the prior book's settings view.\n// We don't exempt \"settings\" here: the user can still click back\n// to it from the (gated) tab strip if they want to delete the\n// new book instead of setting it up.\nwatch(openingGateActive, (active) => {\n if (!active) return;\n if (currentTab.value === \"opening\") return;\n currentTab.value = \"opening\";\n});\n\nvoid refetchBooks();\n</script>\n","<template>\n <!-- Full <AccountingApp> mounted via the openBook tool result.\n Talks to /api/accounting directly for browse / form ops; only\n the entry gate (this mount) runs through the LLM. Pub/sub\n refetches keep multi-tab / sibling-window views in sync. -->\n <div class=\"h-full bg-white flex flex-col\" data-testid=\"accounting-app\">\n <NewBookForm v-if=\"showFirstRunForm\" first-run full-page @created=\"onFirstBookCreated\" />\n <template v-else>\n <header class=\"flex items-center justify-between gap-2 px-3 py-2 border-b border-gray-100 shrink-0\">\n <div class=\"flex items-center gap-2 min-w-0\">\n <span class=\"material-icons text-gray-600\">account_balance</span>\n <h2 class=\"text-lg font-semibold text-gray-800\">{{ t(\"pluginAccounting.title\") }}</h2>\n </div>\n <BookSwitcher\n v-if=\"initialLoadDone\"\n :model-value=\"activeBookId ?? ''\"\n :books=\"books\"\n @update:model-value=\"onBookSelected\"\n @books-changed=\"refetchBooks\"\n @book-created=\"onBookCreated\"\n />\n </header>\n <nav class=\"flex items-center gap-0.5 px-3 py-1.5 border-b border-gray-100 shrink-0 overflow-x-auto\" data-testid=\"accounting-tabs\">\n <button\n v-for=\"tab in visibleTabs\"\n :key=\"tab.key\"\n :class=\"[\n 'h-8 px-2.5 flex items-center gap-1 rounded text-sm whitespace-nowrap',\n deletedNoticeName !== null\n ? 'text-gray-400 cursor-not-allowed'\n : currentTab === tab.key\n ? 'bg-blue-50 text-blue-600 font-medium'\n : 'text-gray-600 hover:bg-gray-50',\n ]\"\n :data-testid=\"`accounting-tab-${tab.key}`\"\n :disabled=\"deletedNoticeName !== null\"\n @click=\"currentTab = tab.key\"\n >\n <span class=\"material-icons text-base\">{{ tab.icon }}</span>\n <span>{{ t(tab.labelKey) }}</span>\n </button>\n </nav>\n <main class=\"flex-1 overflow-auto p-4\">\n <div\n v-if=\"deletedNoticeName !== null\"\n class=\"text-center text-sm text-gray-600 flex flex-col gap-2 items-center justify-center h-full\"\n data-testid=\"accounting-deleted-notice\"\n >\n <span class=\"material-icons text-gray-400\" style=\"font-size: 48px\">delete_outline</span>\n <p class=\"font-medium\" data-testid=\"accounting-deleted-notice-title\">\n {{ t(\"pluginAccounting.deletedNotice.title\", { bookName: deletedNoticeName }) }}\n </p>\n <p class=\"text-xs text-gray-500\">{{ t(\"pluginAccounting.deletedNotice.body\") }}</p>\n </div>\n <p v-else-if=\"loadingBooks && !initialLoadDone\" class=\"text-sm text-gray-400\">{{ t(\"pluginAccounting.common.loading\") }}</p>\n <p v-else-if=\"bookLoadError\" class=\"text-sm text-red-500\" data-testid=\"accounting-load-error\">\n {{ t(\"pluginAccounting.common.error\", { error: bookLoadError }) }}\n </p>\n <p v-else-if=\"!activeBookId\" class=\"text-sm text-gray-500\" data-testid=\"accounting-no-book\">{{ t(\"pluginAccounting.noBook\") }}</p>\n <template v-else-if=\"activeBookId\">\n <JournalList\n v-if=\"currentTab === 'journal'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :country=\"activeCountry\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n :preselect-entry-id=\"journalPreselectEntryId\"\n @edit-opening=\"currentTab = 'opening'\"\n @preselect-consumed=\"journalPreselectEntryId = undefined\"\n />\n <OpeningBalancesForm\n v-else-if=\"currentTab === 'opening'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n @submitted=\"onEntrySubmitted\"\n />\n <AccountsList v-else-if=\"currentTab === 'accounts'\" :book-id=\"activeBookId\" :accounts=\"accounts\" @select-account=\"onAccountSelected\" />\n <Ledger\n v-else-if=\"currentTab === 'ledger'\"\n :book-id=\"activeBookId\"\n :accounts=\"accounts\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n :preselect-account-code=\"ledgerPreselectAccountCode\"\n />\n <BalanceSheet\n v-else-if=\"currentTab === 'balanceSheet'\"\n :book-id=\"activeBookId\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n @select-account=\"onAccountSelected\"\n />\n <ProfitLoss\n v-else-if=\"currentTab === 'profitLoss'\"\n :book-id=\"activeBookId\"\n :currency=\"activeCurrency\"\n :version=\"bookVersion\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n :opening-date=\"activeOpeningDate\"\n @select-account=\"onAccountSelected\"\n />\n <BookSettings\n v-else-if=\"currentTab === 'settings'\"\n :book-id=\"activeBookId\"\n :book-name=\"activeBookName\"\n :currency=\"activeCurrency\"\n :country=\"activeCountry\"\n :fiscal-year-end=\"activeFiscalYearEnd\"\n @deleted=\"onBookDeleted\"\n @books-changed=\"refetchBooks\"\n />\n </template>\n </main>\n </template>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from \"vue\";\nimport { useAccountingI18n } from \"./lang\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport BookSwitcher from \"./components/BookSwitcher.vue\";\nimport NewBookForm from \"./components/NewBookForm.vue\";\nimport JournalList from \"./components/JournalList.vue\";\nimport OpeningBalancesForm from \"./components/OpeningBalancesForm.vue\";\nimport AccountsList from \"./components/AccountsList.vue\";\nimport Ledger from \"./components/Ledger.vue\";\nimport BalanceSheet from \"./components/BalanceSheet.vue\";\nimport ProfitLoss from \"./components/ProfitLoss.vue\";\nimport BookSettings from \"./components/BookSettings.vue\";\nimport { getOpeningBalances, getAccounts, getBooks, type Account, type BookSummary } from \"./api\";\nimport { ACCOUNTING_ACTIONS } from \"../shared\";\nimport { useAccountingChannel, useAccountingBooksChannel } from \"./useAccountingChannel\";\nimport { errorMessage } from \"../shared/errors\";\n\nconst { t } = useAccountingI18n();\n\ninterface AccountingAppPayload {\n kind?: string;\n bookId?: string;\n initialTab?: string;\n /** Dispatch verb stamped onto every accounting tool-result envelope\n * (server/api/routes/accounting.ts dispatch()). We read it here to\n * pick the canvas tab + journal preselect for each PREVIEW action. */\n action?: string;\n /** Present on `addEntries` envelopes — the freshly-built journal\n * entries returned by the service. Each carries a server-stamped\n * `id` we use to highlight the row in JournalList. */\n entries?: { id?: string }[];\n /** Present on `voidEntry` envelopes — the kind=\"void-marker\" row\n * posted alongside the reversing entry. We surface this row (not\n * the reverseEntry) because the marker is the visual \"this entry\n * was voided here\" indicator the user is looking for. */\n markerEntry?: { id?: string };\n}\n\nconst props = defineProps<{ selectedResult?: ToolResultComplete<AccountingAppPayload, AccountingAppPayload> }>();\n\nconst TAB_KEYS = [\"journal\", \"opening\", \"accounts\", \"ledger\", \"balanceSheet\", \"profitLoss\", \"settings\"] as const;\ntype TabKey = (typeof TAB_KEYS)[number];\n\ninterface TabDef {\n key: TabKey;\n icon: string;\n labelKey: string;\n}\n\nconst TABS: readonly TabDef[] = [\n { key: \"journal\", icon: \"list\", labelKey: \"pluginAccounting.tabs.journal\" },\n { key: \"opening\", icon: \"play_arrow\", labelKey: \"pluginAccounting.tabs.opening\" },\n { key: \"accounts\", icon: \"list_alt\", labelKey: \"pluginAccounting.tabs.accounts\" },\n { key: \"ledger\", icon: \"menu_book\", labelKey: \"pluginAccounting.tabs.ledger\" },\n { key: \"balanceSheet\", icon: \"balance\", labelKey: \"pluginAccounting.tabs.balanceSheet\" },\n { key: \"profitLoss\", icon: \"trending_up\", labelKey: \"pluginAccounting.tabs.profitLoss\" },\n { key: \"settings\", icon: \"settings\", labelKey: \"pluginAccounting.tabs.settings\" },\n];\n\nfunction isTabKey(value: string | undefined): value is TabKey {\n return typeof value === \"string\" && (TAB_KEYS as readonly string[]).includes(value);\n}\n\nconst initialPayload = computed<AccountingAppPayload>(() => props.selectedResult?.data ?? props.selectedResult?.jsonData ?? {});\nconst initialTab = computed<TabKey>(() => (isTabKey(initialPayload.value.initialTab) ? initialPayload.value.initialTab : \"journal\"));\n\nconst currentTab = ref<TabKey>(initialTab.value);\nconst books = ref<BookSummary[]>([]);\nconst activeBookId = ref<string | null>(null);\nconst accounts = ref<Account[]>([]);\nconst loadingBooks = ref(true);\n// Sticky once the first books fetch lands. Lets the BookSwitcher stay\n// mounted across subsequent refetches (delete, create, pubsub-driven)\n// so the user sees the dropdown smoothly update its selection rather\n// than having the whole component flash in and out via `v-if`.\nconst initialLoadDone = ref(false);\n// First-run flow: when the user opens the app on a fresh\n// workspace (zero books), we render NewBookForm in full-page\n// mode in place of the regular chrome (header + tabs + main),\n// so the user MUST pick a name + currency before proceeding —\n// no popup, no dismiss. Distinct from the modal opened via\n// BookSwitcher's \"+ New book\" sentinel option, which reuses the\n// same component but with the overlay layout.\nconst showFirstRunForm = ref(false);\nconst firstRunHandled = ref(false);\n// Distinct from \"books is empty\" so we don't show the \"+ New\n// book\" CTA when the real problem is a transport / server failure\n// fetching the list.\nconst bookLoadError = ref<string | null>(null);\n// Tracks whether the active book has an opening entry on file.\n// `null` = unknown / loading; the gate only activates on an\n// explicit `false` so we don't disable tabs during the cold load\n// while the first getOpeningBalances request is still in flight.\nconst hasOpening = ref<boolean | null>(null);\n// Date of the active book's opening entry, plumbed down to the\n// DateRangePicker via the children so \"All\" can resolve to\n// (openingDate → today). `undefined` while loading / for books\n// without an opening on file (the opening gate prevents any tab\n// that would care from being shown in that state).\nconst activeOpeningDate = ref<string | undefined>(undefined);\n// Special \"you just deleted this book\" UI state. When set to a\n// non-null book name, the entire tab strip + main content are\n// replaced by an explicit \"<book> has been deleted — pick another\n// from the dropdown\" panel. Cleared the moment the user picks a\n// book from the BookSwitcher (or creates a new one). The View does\n// NOT auto-route to books[0] because that hides the fact that the\n// previously-active book is gone — issue #1126 (1) calls this\n// experience \"very confusing\".\nconst deletedNoticeName = ref<string | null>(null);\n\nconst activeBook = computed(() => books.value.find((book) => book.id === activeBookId.value) ?? null);\nconst activeBookName = computed(() => activeBook.value?.name ?? \"\");\nconst activeCurrency = computed(() => activeBook.value?.currency ?? \"USD\");\nconst activeCountry = computed(() => activeBook.value?.country);\nconst activeFiscalYearEnd = computed(() => activeBook.value?.fiscalYearEnd);\n\n// Single sync signal: every mutating service function publishes on\n// the accounting book channel after its write, so the sender's own\n// SSE round-trip drives the table/report refetch. No parallel\n// localVersion bump — it only ever fired the same watchers a second\n// time in the same tick.\nconst { version: bookVersion } = useAccountingChannel(activeBookId);\nuseAccountingBooksChannel(() => void refetchBooks());\n\nfunction pickInitialBookId(): string | null {\n // Priority: explicit `initialPayload.bookId` (carried in the\n // tool-result envelope by openBook / createBook / addEntries / …) →\n // first book in the list → null (empty workspace). The candidate\n // is validated against the live book list so a stale id from a\n // deleted book doesn't poison the View.\n if (books.value.length === 0) return null;\n const requested = initialPayload.value.bookId;\n if (requested && books.value.some((book) => book.id === requested)) return requested;\n return books.value[0].id;\n}\n\nasync function refetchBooks(): Promise<void> {\n loadingBooks.value = true;\n bookLoadError.value = null;\n // Capture the current active book BEFORE the fetch so we can\n // surface its name in the deleted-notice panel if the fetch\n // reveals it's gone. Without this snapshot, an SSE-driven refetch\n // racing ahead of the local deleteBook HTTP response would resolve\n // with `activeBook` already pointing at a now-stale entry.\n const previousActive = activeBook.value;\n try {\n const result = await getBooks();\n if (!result.ok) {\n // Surface load failures as a distinct error state so the user\n // doesn't see \"No books yet\" (and the auto-open modal) when\n // the real cause is a transport / server problem.\n bookLoadError.value = result.error;\n return;\n }\n books.value = result.data.books;\n // Sticky-true once a successful fetch lands. Setting it here (in\n // the success branch) rather than in `finally` means a first-load\n // transport / 5xx failure leaves BookSwitcher hidden — the user\n // sees only the `accounting-load-error` message rather than an\n // empty dropdown with a live \"+ New book\" path that has nothing\n // to fall back on.\n initialLoadDone.value = true;\n // While the deleted-notice panel is already up, leave activeBookId\n // alone — the user has to pick the next book themselves via\n // the BookSwitcher (and onBookSelected then clears the notice).\n // Otherwise pickInitialBookId would silently re-select books[0]\n // and undo the entire deletion-state UX.\n if (deletedNoticeName.value === null) {\n const stillExists = activeBookId.value !== null && books.value.some((book) => book.id === activeBookId.value);\n if (!stillExists) {\n // The active book just disappeared from the server's list.\n // Race-source possibilities, all converging here:\n // • local deleteBook → publishBooksChanged → SSE arrives\n // before the HTTP response handler can call onBookDeleted;\n // • a sibling tab / LLM tool deleted the book out-of-band.\n // In all cases the user needs to know what happened — show\n // the deleted-notice panel keyed off the previously-active\n // book's name, rather than silently snapping to books[0].\n // Falls back to the previous pickInitialBookId behaviour only\n // when there was no active book to lose (cold start).\n if (previousActive) {\n activeBookId.value = null;\n deletedNoticeName.value = previousActive.name;\n } else {\n activeBookId.value = pickInitialBookId();\n }\n }\n }\n // Auto-open the New Book modal exactly once on first arrival\n // when the workspace is empty. After that, the user can still\n // open it manually via the \"+ New book\" button.\n if (!firstRunHandled.value && books.value.length === 0) {\n firstRunHandled.value = true;\n showFirstRunForm.value = true;\n }\n } catch (err) {\n bookLoadError.value = errorMessage(err);\n } finally {\n loadingBooks.value = false;\n }\n}\n\nasync function onFirstBookCreated(book: BookSummary): Promise<void> {\n showFirstRunForm.value = false;\n await refetchBooks();\n activeBookId.value = book.id;\n}\n\n// Optimistically insert the new book and set the selection\n// BEFORE the refetch round-trip. Two reasons this beats the\n// previous await-refetch-then-select shape:\n// 1. The pubsub handler `useAccountingBooksChannel` fires its\n// own concurrent `refetchBooks` the instant the server\n// publishes books-changed. With await-then-select, that\n// concurrent refetch's stillExists guard reads the OLD\n// activeBookId (we haven't updated it yet) and — because\n// OLD is still in the books list — leaves the selection\n// pointing at OLD. Our update lands AFTER, but BookSwitcher\n// remounts under `v-if=\"!loadingBooks\"` mid-flight, so the\n// user sees the dropdown stick on OLD.\n// 2. With activeBookId already set to NEW and books pre-\n// populated to include NEW, every concurrent refetch's\n// stillExists check passes for NEW and leaves the selection\n// alone — order-independent by construction.\nasync function onBookCreated(book: BookSummary): Promise<void> {\n if (!books.value.some((existing) => existing.id === book.id)) {\n books.value = [...books.value, book];\n }\n activeBookId.value = book.id;\n // Creating a new book is also the \"exit\" out of the deleted-notice\n // panel — the user explicitly chose the new book, so re-enable the\n // tab strip and let the opening-gate watcher route them to Opening.\n deletedNoticeName.value = null;\n // currentTab may be on \"settings\" (the user opened the create\n // modal from there) — reset to journal so the openingGateActive\n // watcher's \"if (currentTab.value === 'opening') return\" gate\n // doesn't strand the user on settings while the gate is active.\n currentTab.value = \"journal\";\n await refetchBooks();\n}\n\nasync function refetchAccounts(): Promise<void> {\n if (!activeBookId.value) {\n accounts.value = [];\n return;\n }\n const result = await getAccounts(activeBookId.value);\n if (!result.ok) return;\n accounts.value = result.data.accounts;\n}\n\nasync function refetchOpening(): Promise<void> {\n if (!activeBookId.value) {\n hasOpening.value = null;\n activeOpeningDate.value = undefined;\n return;\n }\n const result = await getOpeningBalances(activeBookId.value);\n if (!result.ok) return;\n hasOpening.value = result.data.opening !== null;\n activeOpeningDate.value = result.data.opening?.date;\n}\n\n// A book without an opening on file is in \"gated\" mode: the user\n// must save an opening (empty is fine — see OpeningBalancesForm)\n// before journal / report tabs unlock. Settings stays accessible\n// so the user can delete the book if they don't want to proceed.\nconst openingGateActive = computed(() => activeBookId.value !== null && hasOpening.value === false);\n\n// Gated → only Opening + Settings render in the strip. Ungated →\n// Opening hides itself; users reach the form via the Edit button\n// on the active opening row in the journal, which transiently\n// switches `currentTab` to \"opening\" (kept visible while there).\nconst visibleTabs = computed<readonly TabDef[]>(() => {\n if (openingGateActive.value) return TABS.filter((tab) => tab.key === \"opening\" || tab.key === \"settings\");\n return TABS.filter((tab) => tab.key !== \"opening\" || currentTab.value === \"opening\");\n});\n\nfunction onBookSelected(bookId: string): void {\n activeBookId.value = bookId;\n // Picking a book from the dropdown is the explicit \"I'm done\n // looking at the deleted notice\" exit. Clear it so the tab strip\n // re-enables for the freshly selected book.\n deletedNoticeName.value = null;\n}\n\n// Entry id to surface in JournalList after an `addEntries` tool\n// result lands — the LLM just posted a journal entry and we want\n// the user's eye on the new row. Multi-entry batches highlight the\n// LAST entry only (matches the \"you ended up here\" intent of a\n// scroll-to-cursor).\nconst journalPreselectEntryId = ref<string | undefined>(undefined);\n\n// Account preselected by the Accounts tab → click handoff. Cleared\n// once the user picks a different account from the Ledger's own\n// dropdown so a stale preselection doesn't override later edits.\nconst ledgerPreselectAccountCode = ref<string | undefined>(undefined);\n\nfunction onAccountSelected(code: string): void {\n // Force the ref to a fresh value even when the user clicks the\n // same account a second time — the Ledger's `watch(preselect…)`\n // ignores no-op updates, so we'd otherwise leave the user on a\n // stale Ledger state if they navigated away and clicked back.\n ledgerPreselectAccountCode.value = undefined;\n Promise.resolve().then(() => {\n ledgerPreselectAccountCode.value = code;\n });\n currentTab.value = \"ledger\";\n}\n\nfunction onEntrySubmitted(): void {\n // After saving an opening, switch to the journal so the user\n // immediately sees the unlocked tabs. The server-side\n // publishBookChange triggers the bookVersion watcher over SSE,\n // which refetches hasOpening, so the gate auto-lifts shortly after\n // the tab switch — no manual unlock needed here. Normal entries\n // are now posted from the inline form inside JournalList; that\n // form drives its own dismissal and the journal repaints in\n // place.\n if (currentTab.value === \"opening\") {\n currentTab.value = \"journal\";\n }\n}\n\nasync function onBookDeleted(deletedName: string): Promise<void> {\n // Reset the tab BEFORE awaiting so a fast delete-then-create\n // can't race: if the new book's gate engages while we're still\n // awaiting refetchBooks, the gate watcher needs to see a\n // non-\"settings\" currentTab to route the user to Opening.\n currentTab.value = \"journal\";\n // Drop the active selection so refetchBooks doesn't auto-pick\n // books[0] — the user should see the deleted-notice panel and\n // explicitly switch via the BookSwitcher rather than be silently\n // moved to a different book (issue #1126).\n activeBookId.value = null;\n deletedNoticeName.value = deletedName;\n await refetchBooks();\n}\n\n// Refetch the chart of accounts whenever the active book changes\n// or any pub/sub / child action bumps bookVersion (e.g. an\n// upsertAccount from the Manage Accounts modal, or an LLM-driven\n// upsert in another tab). The list is small JSON; the cost of\n// over-fetching on entry / void / opening events is negligible\n// against the staleness bug it removes.\nwatch(\n () => [activeBookId.value, bookVersion.value],\n () => {\n if (activeBookId.value) void refetchAccounts();\n },\n { immediate: true },\n);\n\n// Drop any leftover Accounts → Ledger preselection when the active\n// book changes. Without this, picking account \"1000\" in book A's\n// Accounts tab and then switching to book B would carry the hint\n// across, so book B's Ledger would auto-select \"1000\" (which may\n// be an unrelated account in B's chart, or absent entirely).\nwatch(activeBookId, () => {\n ledgerPreselectAccountCode.value = undefined;\n});\n\n// Stash a target bookId that we want to land on but haven't been\n// able to apply yet (book not in `books` at the moment the\n// tool-result fired). Cleared as soon as the books list catches up.\nconst pendingTargetBookId = ref<string | null>(null);\n\nfunction applyTargetBookId(target: string): void {\n if (books.value.some((book) => book.id === target)) {\n activeBookId.value = target;\n pendingTargetBookId.value = null;\n return;\n }\n pendingTargetBookId.value = target;\n}\n\n// When the selected tool-result changes (user clicks a different\n// preview card in the sidebar), follow the new result's bookId so\n// the canvas lands on the book that action just touched. Skipped\n// when the new result has no bookId (silent reads / actions that\n// don't carry one). When the target isn't in `books` yet — common\n// race after a fresh `createBook → openBook(bookId)` handoff where\n// the result envelope arrives before refetchBooks completes — the\n// id is stashed and applied by the books watcher below as soon as\n// the list catches up.\nwatch(\n () => initialPayload.value.bookId,\n (next) => {\n if (!next) return;\n applyTargetBookId(next);\n },\n);\n\n// Drains the pending target once `books` includes it (typically\n// after a pub/sub-driven refetch resolves the createBook write).\n// No-op when nothing is pending or the target is still missing.\nwatch(books, () => {\n const pending = pendingTargetBookId.value;\n if (pending) applyTargetBookId(pending);\n});\n\n// Map a PREVIEW action to the canvas tab the user should land on.\n// Honours an explicit `initialTab` from the envelope (the LLM's\n// stated intent) over the action-default below — only `openBook`\n// currently ships initialTab, but the override is plugin-wide.\n//\n// The `balanceSheet` default for openBook / createBook /\n// setOpeningBalances assumes the book has an opening on file. For a\n// fresh book without one, the existing `openingGateActive` watcher\n// redirects to \"opening\" — we don't try to short-circuit that here\n// because hasOpening hasn't necessarily resolved when this runs.\nfunction pickTabForAction(payload: AccountingAppPayload): TabKey | null {\n if (isTabKey(payload.initialTab)) return payload.initialTab;\n switch (payload.action) {\n case ACCOUNTING_ACTIONS.addEntries:\n case ACCOUNTING_ACTIONS.voidEntry:\n return \"journal\";\n case ACCOUNTING_ACTIONS.upsertAccount:\n return \"accounts\";\n case ACCOUNTING_ACTIONS.updateBook:\n return \"settings\";\n case ACCOUNTING_ACTIONS.openBook:\n case ACCOUNTING_ACTIONS.createBook:\n case ACCOUNTING_ACTIONS.setOpeningBalances:\n return \"balanceSheet\";\n default:\n return null;\n }\n}\n\n// For tool results that should auto-expand a row in JournalList,\n// derive the entry id from the action's payload. addEntries picks\n// the LAST entry in the batch (\"you ended up here\" cursor); voidEntry\n// picks the void-MARKER (the visual \"voided here\" indicator), not\n// the reversing entry.\nfunction pickJournalPreselectId(payload: AccountingAppPayload): string | undefined {\n if (payload.action === ACCOUNTING_ACTIONS.addEntries) {\n const entries = Array.isArray(payload.entries) ? payload.entries : [];\n return entries[entries.length - 1]?.id;\n }\n if (payload.action === ACCOUNTING_ACTIONS.voidEntry) {\n return payload.markerEntry?.id;\n }\n return undefined;\n}\n\n// Drive canvas tab + journal preselect from the active tool-result\n// envelope. The route handler stamps `data: { action, bookId, … }`\n// onto every PREVIEW action's response (server/api/routes/\n// accounting.ts dispatch + PREVIEW_ACTIONS). `immediate: true` so a\n// cold open with the result already selected (e.g., reload after\n// the LLM dispatched) routes to the right surface too.\n//\n// Preselect is *always* assigned (not `if (preselect)`) so a\n// subsequent non-addEntries/voidEntry tool result clears any stale\n// id left over from a prior addEntries the user has already seen —\n// otherwise the next JournalList remount would replay it. The child\n// also emits `preselectConsumed` after expanding for the same\n// reason.\nwatch(\n () => initialPayload.value,\n (payload) => {\n const targetTab = pickTabForAction(payload);\n if (targetTab) currentTab.value = targetTab;\n journalPreselectEntryId.value = pickJournalPreselectId(payload);\n },\n { immediate: true },\n);\n\n// Drop the journal preselect on a real book SWITCH — leftover ids\n// from the prior book don't exist in the new one. The cold-load\n// transition (null → bookId) doesn't qualify: refetchBooks resolves\n// activeBookId asynchronously and would otherwise clobber a\n// preselect the addEntries watcher just set on initial mount.\nwatch(activeBookId, (_next, prev) => {\n if (!prev) return;\n journalPreselectEntryId.value = undefined;\n});\n\n// Refetch the opening status whenever the active book changes or\n// any pub/sub / child action bumps bookVersion (e.g. an opening\n// got saved or voided). Clears hasOpening when the book goes null\n// so a stale \"true\" doesn't carry over between books.\nwatch(\n () => [activeBookId.value, bookVersion.value],\n () => void refetchOpening(),\n { immediate: true },\n);\n\n// Force-route to the Opening tab whenever the gate engages.\n// Other tabs are hidden from the strip while gated, but this\n// watcher handles the programmatic case where currentTab still\n// points at a now-hidden tab (book switch, initial mount with a\n// no-opening book, LLM-supplied initialTab pointing at a gated\n// tab, or fresh-book creation right after deleting from the\n// settings tab) — without it, `<main>` would render nothing or\n// the user would be stranded on the prior book's settings view.\n// We don't exempt \"settings\" here: the user can still click back\n// to it from the (gated) tab strip if they want to delete the\n// new book instead of setting it up.\nwatch(openingGateActive, (active) => {\n if (!active) return;\n if (currentTab.value === \"opening\") return;\n currentTab.value = \"opening\";\n});\n\nvoid refetchBooks();\n</script>\n","<template>\n <!-- Compact inline summary for non-openBook tool results. The\n openBook envelope routes to View.vue (full app) instead of\n this component; everything that lands here is a\n compact-result action (addEntries, getReport, …). -->\n <div class=\"text-sm text-gray-700\" data-testid=\"accounting-preview\">\n <span class=\"material-icons text-base align-middle mr-1\">account_balance</span>\n <span>{{ summary }}</span>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useAccountingI18n } from \"./lang\";\nimport { formatAmountNumeric } from \"../shared\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ data?: unknown; jsonData?: Record<string, unknown> }>();\n\ninterface BalanceSheetSection {\n type: string;\n total?: number;\n}\ninterface BalanceSheetLike {\n balanceSheet?: { asOf?: string; sections?: BalanceSheetSection[]; imbalance?: number };\n}\ninterface ProfitLossLike {\n profitLoss?: { from?: string; to?: string; netIncome?: number };\n}\ninterface EntriesLike {\n entries?: { id?: string; date?: string }[];\n}\ninterface BookLike {\n book?: { id?: string; name?: string };\n}\n\n// Each summarise* helper returns null when its branch doesn't apply,\n// keeping the dispatch in `summary` linear (no nested if-trees).\n\nfunction summariseError(json: Record<string, unknown>): string | null {\n const { error } = json as { error?: unknown };\n if (typeof error !== \"string\") return null;\n return t(\"pluginAccounting.previewError\", { error });\n}\n\nfunction summariseEntry(json: Record<string, unknown>): string | null {\n // addEntries returns `{ entries: [...] }`. The compact preview\n // card shows one date — use the first entry's so single-entry\n // batches (the common case from the manual UI) read naturally\n // and multi-entry batches still anchor to a meaningful date.\n const { entries } = json as EntriesLike;\n if (!Array.isArray(entries) || entries.length === 0) return null;\n const [first] = entries;\n if (!first?.id || !first?.date) return null;\n return t(\"pluginAccounting.preview.entry\", { date: first.date });\n}\n\nfunction summarisePl(json: Record<string, unknown>): string | null {\n const { profitLoss } = json as ProfitLossLike;\n if (!profitLoss || typeof profitLoss.netIncome !== \"number\") return null;\n return t(\"pluginAccounting.preview.pl\", {\n from: profitLoss.from ?? \"?\",\n to: profitLoss.to ?? \"?\",\n net: formatAmountNumeric(profitLoss.netIncome),\n });\n}\n\nfunction summariseBs(json: Record<string, unknown>): string | null {\n const { balanceSheet } = json as BalanceSheetLike;\n if (!balanceSheet?.asOf || !balanceSheet.sections) return null;\n const assets = balanceSheet.sections.find((section) => section.type === \"asset\");\n return t(\"pluginAccounting.preview.bs\", {\n date: balanceSheet.asOf,\n assets: assets ? formatAmountNumeric(assets.total ?? 0) : \"?\",\n });\n}\n\nfunction summariseBook(json: Record<string, unknown>): string | null {\n const { book } = json as BookLike;\n if (!book?.id || !book?.name) return null;\n return t(\"pluginAccounting.preview.bookCreated\", { name: book.name, id: book.id });\n}\n\nfunction summariseFallback(json: Record<string, unknown>): string {\n const { bookId } = json as { bookId?: unknown };\n if (typeof bookId === \"string\") return t(\"pluginAccounting.previewSummary\", { bookId });\n return t(\"pluginAccounting.previewGeneric\");\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n // Some renderers pass the structured payload via `data`, others\n // via `jsonData`. Accept either so a tool-result like\n // `{ entry: ... }` resolves to the right summariser regardless\n // of which prop the host harness picks.\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : {};\n}\n\nconst summary = computed<string>(() => {\n const json = { ...asObject(props.data), ...asObject(props.jsonData) };\n return summariseError(json) ?? summariseEntry(json) ?? summarisePl(json) ?? summariseBs(json) ?? summariseBook(json) ?? summariseFallback(json);\n});\n</script>\n","<template>\n <!-- Compact inline summary for non-openBook tool results. The\n openBook envelope routes to View.vue (full app) instead of\n this component; everything that lands here is a\n compact-result action (addEntries, getReport, …). -->\n <div class=\"text-sm text-gray-700\" data-testid=\"accounting-preview\">\n <span class=\"material-icons text-base align-middle mr-1\">account_balance</span>\n <span>{{ summary }}</span>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useAccountingI18n } from \"./lang\";\nimport { formatAmountNumeric } from \"../shared\";\n\nconst { t } = useAccountingI18n();\n\nconst props = defineProps<{ data?: unknown; jsonData?: Record<string, unknown> }>();\n\ninterface BalanceSheetSection {\n type: string;\n total?: number;\n}\ninterface BalanceSheetLike {\n balanceSheet?: { asOf?: string; sections?: BalanceSheetSection[]; imbalance?: number };\n}\ninterface ProfitLossLike {\n profitLoss?: { from?: string; to?: string; netIncome?: number };\n}\ninterface EntriesLike {\n entries?: { id?: string; date?: string }[];\n}\ninterface BookLike {\n book?: { id?: string; name?: string };\n}\n\n// Each summarise* helper returns null when its branch doesn't apply,\n// keeping the dispatch in `summary` linear (no nested if-trees).\n\nfunction summariseError(json: Record<string, unknown>): string | null {\n const { error } = json as { error?: unknown };\n if (typeof error !== \"string\") return null;\n return t(\"pluginAccounting.previewError\", { error });\n}\n\nfunction summariseEntry(json: Record<string, unknown>): string | null {\n // addEntries returns `{ entries: [...] }`. The compact preview\n // card shows one date — use the first entry's so single-entry\n // batches (the common case from the manual UI) read naturally\n // and multi-entry batches still anchor to a meaningful date.\n const { entries } = json as EntriesLike;\n if (!Array.isArray(entries) || entries.length === 0) return null;\n const [first] = entries;\n if (!first?.id || !first?.date) return null;\n return t(\"pluginAccounting.preview.entry\", { date: first.date });\n}\n\nfunction summarisePl(json: Record<string, unknown>): string | null {\n const { profitLoss } = json as ProfitLossLike;\n if (!profitLoss || typeof profitLoss.netIncome !== \"number\") return null;\n return t(\"pluginAccounting.preview.pl\", {\n from: profitLoss.from ?? \"?\",\n to: profitLoss.to ?? \"?\",\n net: formatAmountNumeric(profitLoss.netIncome),\n });\n}\n\nfunction summariseBs(json: Record<string, unknown>): string | null {\n const { balanceSheet } = json as BalanceSheetLike;\n if (!balanceSheet?.asOf || !balanceSheet.sections) return null;\n const assets = balanceSheet.sections.find((section) => section.type === \"asset\");\n return t(\"pluginAccounting.preview.bs\", {\n date: balanceSheet.asOf,\n assets: assets ? formatAmountNumeric(assets.total ?? 0) : \"?\",\n });\n}\n\nfunction summariseBook(json: Record<string, unknown>): string | null {\n const { book } = json as BookLike;\n if (!book?.id || !book?.name) return null;\n return t(\"pluginAccounting.preview.bookCreated\", { name: book.name, id: book.id });\n}\n\nfunction summariseFallback(json: Record<string, unknown>): string {\n const { bookId } = json as { bookId?: unknown };\n if (typeof bookId === \"string\") return t(\"pluginAccounting.previewSummary\", { bookId });\n return t(\"pluginAccounting.previewGeneric\");\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n // Some renderers pass the structured payload via `data`, others\n // via `jsonData`. Accept either so a tool-result like\n // `{ entry: ... }` resolves to the right summariser regardless\n // of which prop the host harness picks.\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : {};\n}\n\nconst summary = computed<string>(() => {\n const json = { ...asObject(props.data), ...asObject(props.jsonData) };\n return summariseError(json) ?? summariseEntry(json) ?? summarisePl(json) ?? summariseBs(json) ?? summariseBook(json) ?? summariseFallback(json);\n});\n</script>\n"],"mappings":"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"}
|