@mulmoclaude/accounting-plugin 0.1.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.
Files changed (112) hide show
  1. package/dist/server/accountNormalize.d.ts +3 -0
  2. package/dist/server/accountNormalize.d.ts.map +1 -0
  3. package/dist/server/atomic.d.ts +13 -0
  4. package/dist/server/atomic.d.ts.map +1 -0
  5. package/dist/server/context.d.ts +39 -0
  6. package/dist/server/context.d.ts.map +1 -0
  7. package/dist/server/defaultAccounts.d.ts +3 -0
  8. package/dist/server/defaultAccounts.d.ts.map +1 -0
  9. package/dist/server/eventPublisher.d.ts +14 -0
  10. package/dist/server/eventPublisher.d.ts.map +1 -0
  11. package/dist/server/http.d.ts +3 -0
  12. package/dist/server/http.d.ts.map +1 -0
  13. package/dist/server/index.d.ts +6 -0
  14. package/dist/server/index.d.ts.map +1 -0
  15. package/dist/server/io.d.ts +67 -0
  16. package/dist/server/io.d.ts.map +1 -0
  17. package/dist/server/journal.d.ts +74 -0
  18. package/dist/server/journal.d.ts.map +1 -0
  19. package/dist/server/openingBalances.d.ts +30 -0
  20. package/dist/server/openingBalances.d.ts.map +1 -0
  21. package/dist/server/report.d.ts +98 -0
  22. package/dist/server/report.d.ts.map +1 -0
  23. package/dist/server/router.d.ts +7 -0
  24. package/dist/server/router.d.ts.map +1 -0
  25. package/dist/server/service.d.ts +148 -0
  26. package/dist/server/service.d.ts.map +1 -0
  27. package/dist/server/snapshotCache.d.ts +52 -0
  28. package/dist/server/snapshotCache.d.ts.map +1 -0
  29. package/dist/server/timeSeries.d.ts +47 -0
  30. package/dist/server/timeSeries.d.ts.map +1 -0
  31. package/dist/server/types.d.ts +134 -0
  32. package/dist/server/types.d.ts.map +1 -0
  33. package/dist/server.cjs +2101 -0
  34. package/dist/server.cjs.map +1 -0
  35. package/dist/server.js +2074 -0
  36. package/dist/server.js.map +1 -0
  37. package/dist/shared/actions.d.ts +19 -0
  38. package/dist/shared/actions.d.ts.map +1 -0
  39. package/dist/shared/channels.d.ts +46 -0
  40. package/dist/shared/channels.d.ts.map +1 -0
  41. package/dist/shared/countries.d.ts +51 -0
  42. package/dist/shared/countries.d.ts.map +1 -0
  43. package/dist/shared/currencies.d.ts +34 -0
  44. package/dist/shared/currencies.d.ts.map +1 -0
  45. package/dist/shared/dates.d.ts +15 -0
  46. package/dist/shared/dates.d.ts.map +1 -0
  47. package/dist/shared/errors.d.ts +2 -0
  48. package/dist/shared/errors.d.ts.map +1 -0
  49. package/dist/shared/fiscalYear.d.ts +22 -0
  50. package/dist/shared/fiscalYear.d.ts.map +1 -0
  51. package/dist/shared/index.d.ts +9 -0
  52. package/dist/shared/index.d.ts.map +1 -0
  53. package/dist/shared/timeSeriesEnums.d.ts +5 -0
  54. package/dist/shared/timeSeriesEnums.d.ts.map +1 -0
  55. package/dist/shared.cjs +466 -0
  56. package/dist/shared.cjs.map +1 -0
  57. package/dist/shared.js +432 -0
  58. package/dist/shared.js.map +1 -0
  59. package/dist/style.css +1255 -0
  60. package/dist/vue/Preview.vue.d.ts +8 -0
  61. package/dist/vue/Preview.vue.d.ts.map +1 -0
  62. package/dist/vue/View.vue.d.ts +30 -0
  63. package/dist/vue/View.vue.d.ts.map +1 -0
  64. package/dist/vue/api.d.ts +269 -0
  65. package/dist/vue/api.d.ts.map +1 -0
  66. package/dist/vue/components/AccountEditor.vue.d.ts +19 -0
  67. package/dist/vue/components/AccountEditor.vue.d.ts.map +1 -0
  68. package/dist/vue/components/AccountRow.vue.d.ts +14 -0
  69. package/dist/vue/components/AccountRow.vue.d.ts.map +1 -0
  70. package/dist/vue/components/AccountsList.vue.d.ts +15 -0
  71. package/dist/vue/components/AccountsList.vue.d.ts.map +1 -0
  72. package/dist/vue/components/AccountsModal.vue.d.ts +15 -0
  73. package/dist/vue/components/AccountsModal.vue.d.ts.map +1 -0
  74. package/dist/vue/components/BalanceSheet.vue.d.ts +13 -0
  75. package/dist/vue/components/BalanceSheet.vue.d.ts.map +1 -0
  76. package/dist/vue/components/BookSettings.vue.d.ts +18 -0
  77. package/dist/vue/components/BookSettings.vue.d.ts.map +1 -0
  78. package/dist/vue/components/BookSwitcher.vue.d.ts +17 -0
  79. package/dist/vue/components/BookSwitcher.vue.d.ts.map +1 -0
  80. package/dist/vue/components/DateRangePicker.vue.d.ts +19 -0
  81. package/dist/vue/components/DateRangePicker.vue.d.ts.map +1 -0
  82. package/dist/vue/components/JournalEntryForm.vue.d.ts +19 -0
  83. package/dist/vue/components/JournalEntryForm.vue.d.ts.map +1 -0
  84. package/dist/vue/components/JournalList.vue.d.ts +30 -0
  85. package/dist/vue/components/JournalList.vue.d.ts.map +1 -0
  86. package/dist/vue/components/Ledger.vue.d.ts +21 -0
  87. package/dist/vue/components/Ledger.vue.d.ts.map +1 -0
  88. package/dist/vue/components/NewBookForm.vue.d.ts +20 -0
  89. package/dist/vue/components/NewBookForm.vue.d.ts.map +1 -0
  90. package/dist/vue/components/OpeningBalancesForm.vue.d.ts +15 -0
  91. package/dist/vue/components/OpeningBalancesForm.vue.d.ts.map +1 -0
  92. package/dist/vue/components/ProfitLoss.vue.d.ts +19 -0
  93. package/dist/vue/components/ProfitLoss.vue.d.ts.map +1 -0
  94. package/dist/vue/components/accountDraft.d.ts +8 -0
  95. package/dist/vue/components/accountDraft.d.ts.map +1 -0
  96. package/dist/vue/components/accountNumbering.d.ts +20 -0
  97. package/dist/vue/components/accountNumbering.d.ts.map +1 -0
  98. package/dist/vue/components/accountValidation.d.ts +34 -0
  99. package/dist/vue/components/accountValidation.d.ts.map +1 -0
  100. package/dist/vue/components/useLatestRequest.d.ts +10 -0
  101. package/dist/vue/components/useLatestRequest.d.ts.map +1 -0
  102. package/dist/vue/hostContext.d.ts +31 -0
  103. package/dist/vue/hostContext.d.ts.map +1 -0
  104. package/dist/vue/index.d.ts +7 -0
  105. package/dist/vue/index.d.ts.map +1 -0
  106. package/dist/vue/useAccountingChannel.d.ts +13 -0
  107. package/dist/vue/useAccountingChannel.d.ts.map +1 -0
  108. package/dist/vue.cjs +3641 -0
  109. package/dist/vue.cjs.map +1 -0
  110. package/dist/vue.js +3638 -0
  111. package/dist/vue.js.map +1 -0
  112. package/package.json +74 -0
@@ -0,0 +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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"}