@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
package/dist/vue.js ADDED
@@ -0,0 +1,3638 @@
1
+ import { ACCOUNTING_ACTIONS, ACCOUNTING_BOOKS_CHANNEL, FISCAL_YEAR_ENDS, SUPPORTED_COUNTRY_CODES, SUPPORTED_CURRENCY_CODES, bookChannel, countryHasFeature, currentFiscalYearRange, currentQuarterRange, decemberOfPreviousYearString, errorMessage, formatAmount, formatAmountNumeric, inputStepFor, isSupportedCountryCode, lastMonthOfPreviousQuarterString, localDateString, localMonthString, localizedCountryName, localizedCurrencyName, previousFiscalYearRange, previousMonthString, previousQuarterRange, resolveFiscalYearEnd } from "./shared.js";
2
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, onMounted, onUnmounted, openBlock, reactive, ref, renderList, toDisplayString, unref, vModelSelect, vModelText, watch, withDirectives, withKeys, withModifiers } from "vue";
3
+ import { useI18n } from "vue-i18n";
4
+ //#region src/vue/hostContext.ts
5
+ var ctx = null;
6
+ /** Called once by the host before any accounting View mounts. */
7
+ function configureAccountingHost(context) {
8
+ ctx = context;
9
+ }
10
+ function requireCtx() {
11
+ if (!ctx) throw new Error("@mulmoclaude/accounting-plugin: configureAccountingHost() must be called before the accounting View mounts");
12
+ return ctx;
13
+ }
14
+ function hostApiCall(path, opts) {
15
+ return requireCtx().apiCall(path, opts);
16
+ }
17
+ function hostSubscribe(channel, handler) {
18
+ return requireCtx().subscribe(channel, handler);
19
+ }
20
+ //#endregion
21
+ //#region src/vue/api.ts
22
+ var DISPATCH_URL = "/api/accounting";
23
+ var DISPATCH_METHOD = "POST";
24
+ function call(action, args = {}) {
25
+ return hostApiCall(DISPATCH_URL, {
26
+ method: DISPATCH_METHOD,
27
+ body: {
28
+ action,
29
+ ...args
30
+ }
31
+ });
32
+ }
33
+ function getBooks() {
34
+ return call(ACCOUNTING_ACTIONS.getBooks);
35
+ }
36
+ function createBook(input) {
37
+ return call(ACCOUNTING_ACTIONS.createBook, input);
38
+ }
39
+ function updateBook(input) {
40
+ return call(ACCOUNTING_ACTIONS.updateBook, input);
41
+ }
42
+ function deleteBook(bookId) {
43
+ return call(ACCOUNTING_ACTIONS.deleteBook, {
44
+ bookId,
45
+ confirm: true
46
+ });
47
+ }
48
+ function getAccounts(bookId) {
49
+ return call(ACCOUNTING_ACTIONS.getAccounts, { bookId });
50
+ }
51
+ function upsertAccount(account, bookId) {
52
+ return call(ACCOUNTING_ACTIONS.upsertAccount, {
53
+ account,
54
+ bookId
55
+ });
56
+ }
57
+ function addEntries(input) {
58
+ return call(ACCOUNTING_ACTIONS.addEntries, input);
59
+ }
60
+ function voidEntry(input) {
61
+ return call(ACCOUNTING_ACTIONS.voidEntry, input);
62
+ }
63
+ function getJournalEntries(input) {
64
+ return call(ACCOUNTING_ACTIONS.getJournalEntries, input);
65
+ }
66
+ function getOpeningBalances(bookId) {
67
+ return call(ACCOUNTING_ACTIONS.getOpeningBalances, { bookId });
68
+ }
69
+ function setOpeningBalances(input) {
70
+ return call(ACCOUNTING_ACTIONS.setOpeningBalances, input);
71
+ }
72
+ function getBalanceSheet(period, bookId) {
73
+ return call(ACCOUNTING_ACTIONS.getReport, {
74
+ kind: "balance",
75
+ period,
76
+ bookId
77
+ });
78
+ }
79
+ function getProfitLoss(period, bookId) {
80
+ return call(ACCOUNTING_ACTIONS.getReport, {
81
+ kind: "pl",
82
+ period,
83
+ bookId
84
+ });
85
+ }
86
+ function getLedger(accountCode, period, bookId) {
87
+ return call(ACCOUNTING_ACTIONS.getReport, {
88
+ kind: "ledger",
89
+ accountCode,
90
+ period,
91
+ bookId
92
+ });
93
+ }
94
+ function rebuildSnapshots(bookId) {
95
+ return call(ACCOUNTING_ACTIONS.rebuildSnapshots, { bookId });
96
+ }
97
+ //#endregion
98
+ //#region src/vue/components/NewBookForm.vue?vue&type=script&setup=true&lang.ts
99
+ var _hoisted_1$15 = { class: "text-base font-semibold" };
100
+ var _hoisted_2$14 = {
101
+ key: 0,
102
+ class: "text-xs text-gray-500",
103
+ "data-testid": "accounting-new-book-firstrun"
104
+ };
105
+ var _hoisted_3$14 = { class: "text-sm flex flex-col gap-1" };
106
+ var _hoisted_4$14 = { class: "text-sm flex flex-col gap-1" };
107
+ var _hoisted_5$14 = ["value"];
108
+ var _hoisted_6$13 = { class: "text-sm flex flex-col gap-1" };
109
+ var _hoisted_7$12 = { value: "" };
110
+ var _hoisted_8$12 = ["value"];
111
+ var _hoisted_9$11 = { class: "text-xs text-gray-500" };
112
+ var _hoisted_10$11 = { class: "text-sm flex flex-col gap-1" };
113
+ var _hoisted_11$10 = ["value"];
114
+ var _hoisted_12$10 = { class: "text-xs text-gray-500" };
115
+ var _hoisted_13$10 = {
116
+ key: 1,
117
+ class: "text-xs text-red-500",
118
+ "data-testid": "accounting-new-book-error"
119
+ };
120
+ var _hoisted_14$8 = { class: "flex justify-end gap-2 mt-1" };
121
+ var _hoisted_15$7 = ["disabled"];
122
+ //#endregion
123
+ //#region src/vue/components/NewBookForm.vue
124
+ var NewBookForm_default = /* @__PURE__ */ defineComponent({
125
+ __name: "NewBookForm",
126
+ props: {
127
+ firstRun: {
128
+ type: Boolean,
129
+ default: false
130
+ },
131
+ cancelable: {
132
+ type: Boolean,
133
+ default: true
134
+ },
135
+ fullPage: {
136
+ type: Boolean,
137
+ default: false
138
+ }
139
+ },
140
+ emits: ["cancel", "created"],
141
+ setup(__props, { emit: __emit }) {
142
+ const { t, locale } = useI18n();
143
+ function regionFromLocaleTag(tag) {
144
+ try {
145
+ const { region } = new Intl.Locale(tag).maximize();
146
+ if (region && SUPPORTED_COUNTRY_CODES.includes(region)) return region;
147
+ } catch {}
148
+ return "";
149
+ }
150
+ function guessDefaultCountry(uiLocaleTag) {
151
+ const fromUi = regionFromLocaleTag(uiLocaleTag);
152
+ if (fromUi !== "") return fromUi;
153
+ return regionFromLocaleTag(typeof navigator !== "undefined" && typeof navigator.language === "string" ? navigator.language : "");
154
+ }
155
+ const props = __props;
156
+ const emit = __emit;
157
+ const name = ref("");
158
+ const currency = ref("USD");
159
+ const country = ref(guessDefaultCountry(locale.value));
160
+ const fiscalYearEnd = ref("Q4");
161
+ const creating = ref(false);
162
+ const error = ref(null);
163
+ const nameInput = ref(null);
164
+ onMounted(() => {
165
+ nextTick(() => nameInput.value?.focus());
166
+ });
167
+ const options = computed(() => SUPPORTED_CURRENCY_CODES.map((code) => ({
168
+ code,
169
+ label: `${code} — ${localizedCurrencyName(code, locale.value)}`
170
+ })));
171
+ const countryOptions = computed(() => SUPPORTED_COUNTRY_CODES.map((code) => ({
172
+ code,
173
+ label: `${code} — ${localizedCountryName(code, locale.value)}`
174
+ })));
175
+ const fiscalYearEndOptions = computed(() => FISCAL_YEAR_ENDS.map((value) => ({
176
+ value,
177
+ label: t(`pluginAccounting.bookSwitcher.fiscalYearEnd${value}`)
178
+ })));
179
+ const wrapperClass = computed(() => 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");
180
+ const showCancel = computed(() => props.cancelable && !props.fullPage);
181
+ function onBackdropClick() {
182
+ if (props.fullPage) return;
183
+ onCancel();
184
+ }
185
+ function onCancel() {
186
+ if (!props.cancelable) return;
187
+ emit("cancel");
188
+ }
189
+ async function onSubmit() {
190
+ if (creating.value) return;
191
+ creating.value = true;
192
+ error.value = null;
193
+ try {
194
+ const pickedCountry = country.value === "" ? void 0 : country.value;
195
+ const result = await createBook({
196
+ name: name.value.trim(),
197
+ currency: currency.value,
198
+ country: pickedCountry,
199
+ fiscalYearEnd: fiscalYearEnd.value
200
+ });
201
+ if (!result.ok) {
202
+ error.value = result.error;
203
+ return;
204
+ }
205
+ emit("created", result.data.book);
206
+ } finally {
207
+ creating.value = false;
208
+ }
209
+ }
210
+ return (_ctx, _cache) => {
211
+ return openBlock(), createElementBlock("div", {
212
+ class: normalizeClass(wrapperClass.value),
213
+ "data-testid": "accounting-new-book-modal",
214
+ onClick: withModifiers(onBackdropClick, ["self"])
215
+ }, [createElementVNode("form", {
216
+ class: "bg-white p-4 rounded shadow-lg w-96 flex flex-col gap-3",
217
+ "data-testid": "accounting-new-book-form",
218
+ onSubmit: withModifiers(onSubmit, ["prevent"])
219
+ }, [
220
+ createElementVNode("h3", _hoisted_1$15, toDisplayString(unref(t)("pluginAccounting.bookSwitcher.newBook")), 1),
221
+ __props.firstRun ? (openBlock(), createElementBlock("p", _hoisted_2$14, toDisplayString(unref(t)("pluginAccounting.bookSwitcher.firstRunHint")), 1)) : createCommentVNode("", true),
222
+ createElementVNode("label", _hoisted_3$14, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.bookSwitcher.nameLabel")) + " ", 1), withDirectives(createElementVNode("input", {
223
+ ref_key: "nameInput",
224
+ ref: nameInput,
225
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => name.value = $event),
226
+ required: "",
227
+ class: "h-8 px-2 rounded border border-gray-300 text-sm",
228
+ "data-testid": "accounting-new-book-name"
229
+ }, null, 512), [[vModelText, name.value]])]),
230
+ createElementVNode("label", _hoisted_4$14, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.bookSwitcher.currencyLabel")) + " ", 1), withDirectives(createElementVNode("select", {
231
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => currency.value = $event),
232
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
233
+ "data-testid": "accounting-new-book-currency"
234
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(options.value, (opt) => {
235
+ return openBlock(), createElementBlock("option", {
236
+ key: opt.code,
237
+ value: opt.code
238
+ }, toDisplayString(opt.label), 9, _hoisted_5$14);
239
+ }), 128))], 512), [[vModelSelect, currency.value]])]),
240
+ createElementVNode("label", _hoisted_6$13, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.bookSwitcher.countryLabel")) + " ", 1), withDirectives(createElementVNode("select", {
241
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => country.value = $event),
242
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
243
+ "data-testid": "accounting-new-book-country"
244
+ }, [createElementVNode("option", _hoisted_7$12, toDisplayString(unref(t)("pluginAccounting.bookSwitcher.countryPlaceholder")), 1), (openBlock(true), createElementBlock(Fragment, null, renderList(countryOptions.value, (opt) => {
245
+ return openBlock(), createElementBlock("option", {
246
+ key: opt.code,
247
+ value: opt.code
248
+ }, toDisplayString(opt.label), 9, _hoisted_8$12);
249
+ }), 128))], 512), [[vModelSelect, country.value]])]),
250
+ createElementVNode("p", _hoisted_9$11, toDisplayString(unref(t)("pluginAccounting.bookSwitcher.countryHint")), 1),
251
+ createElementVNode("label", _hoisted_10$11, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.bookSwitcher.fiscalYearEndLabel")) + " ", 1), withDirectives(createElementVNode("select", {
252
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => fiscalYearEnd.value = $event),
253
+ required: "",
254
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
255
+ "data-testid": "accounting-new-book-fiscal-year-end"
256
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(fiscalYearEndOptions.value, (opt) => {
257
+ return openBlock(), createElementBlock("option", {
258
+ key: opt.value,
259
+ value: opt.value
260
+ }, toDisplayString(opt.label), 9, _hoisted_11$10);
261
+ }), 128))], 512), [[vModelSelect, fiscalYearEnd.value]])]),
262
+ createElementVNode("p", _hoisted_12$10, toDisplayString(unref(t)("pluginAccounting.bookSwitcher.fiscalYearEndHint")), 1),
263
+ error.value ? (openBlock(), createElementBlock("p", _hoisted_13$10, toDisplayString(error.value), 1)) : createCommentVNode("", true),
264
+ createElementVNode("div", _hoisted_14$8, [showCancel.value ? (openBlock(), createElementBlock("button", {
265
+ key: 0,
266
+ type: "button",
267
+ class: "h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-700 hover:bg-gray-50",
268
+ onClick: onCancel
269
+ }, toDisplayString(unref(t)("pluginAccounting.common.cancel")), 1)) : createCommentVNode("", true), createElementVNode("button", {
270
+ type: "submit",
271
+ class: "h-8 px-2.5 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm",
272
+ disabled: creating.value,
273
+ "data-testid": "accounting-new-book-submit"
274
+ }, toDisplayString(creating.value ? unref(t)("pluginAccounting.common.loading") : unref(t)("pluginAccounting.bookSwitcher.create")), 9, _hoisted_15$7)])
275
+ ], 32)], 2);
276
+ };
277
+ }
278
+ });
279
+ //#endregion
280
+ //#region src/vue/components/BookSwitcher.vue?vue&type=script&setup=true&lang.ts
281
+ var _hoisted_1$14 = { class: "flex items-center gap-2" };
282
+ var _hoisted_2$13 = {
283
+ class: "text-xs text-gray-500",
284
+ for: "accounting-book-select"
285
+ };
286
+ var _hoisted_3$13 = ["value"];
287
+ var _hoisted_4$13 = {
288
+ key: 0,
289
+ value: "",
290
+ disabled: ""
291
+ };
292
+ var _hoisted_5$13 = ["value"];
293
+ var NEW_BOOK_SENTINEL = "__new__";
294
+ //#endregion
295
+ //#region src/vue/components/BookSwitcher.vue
296
+ var BookSwitcher_default = /* @__PURE__ */ defineComponent({
297
+ __name: "BookSwitcher",
298
+ props: {
299
+ modelValue: {},
300
+ books: {}
301
+ },
302
+ emits: [
303
+ "update:modelValue",
304
+ "books-changed",
305
+ "book-created"
306
+ ],
307
+ setup(__props, { emit: __emit }) {
308
+ const { t } = useI18n();
309
+ const props = __props;
310
+ const emit = __emit;
311
+ const showNewBook = ref(false);
312
+ function formatBookOption(book) {
313
+ const suffix = book.country ? `${book.currency} · ${book.country}` : book.currency;
314
+ return `${book.name} (${suffix})`;
315
+ }
316
+ function onSelect(event) {
317
+ const target = event.target;
318
+ const bookId = target.value;
319
+ if (bookId === NEW_BOOK_SENTINEL) {
320
+ target.value = props.modelValue;
321
+ showNewBook.value = true;
322
+ return;
323
+ }
324
+ if (bookId === props.modelValue) return;
325
+ emit("update:modelValue", bookId);
326
+ }
327
+ function onCreated(book) {
328
+ showNewBook.value = false;
329
+ emit("book-created", book);
330
+ }
331
+ return (_ctx, _cache) => {
332
+ return openBlock(), createElementBlock("div", _hoisted_1$14, [
333
+ createElementVNode("label", _hoisted_2$13, toDisplayString(unref(t)("pluginAccounting.bookSwitcher.label")), 1),
334
+ createElementVNode("select", {
335
+ id: "accounting-book-select",
336
+ value: __props.modelValue,
337
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
338
+ "data-testid": "accounting-book-select",
339
+ onChange: onSelect
340
+ }, [
341
+ __props.modelValue === "" ? (openBlock(), createElementBlock("option", _hoisted_4$13, toDisplayString(unref(t)("pluginAccounting.bookSwitcher.placeholder")), 1)) : createCommentVNode("", true),
342
+ (openBlock(true), createElementBlock(Fragment, null, renderList(__props.books, (book) => {
343
+ return openBlock(), createElementBlock("option", {
344
+ key: book.id,
345
+ value: book.id
346
+ }, toDisplayString(formatBookOption(book)), 9, _hoisted_5$13);
347
+ }), 128)),
348
+ _cache[1] || (_cache[1] = createElementVNode("option", { disabled: "" }, "──────────", -1)),
349
+ createElementVNode("option", {
350
+ value: NEW_BOOK_SENTINEL,
351
+ "data-testid": "accounting-new-book-option"
352
+ }, "+ " + toDisplayString(unref(t)("pluginAccounting.bookSwitcher.newBook")), 1)
353
+ ], 40, _hoisted_3$13),
354
+ showNewBook.value ? (openBlock(), createBlock(NewBookForm_default, {
355
+ key: 0,
356
+ onCancel: _cache[0] || (_cache[0] = ($event) => showNewBook.value = false),
357
+ onCreated
358
+ })) : createCommentVNode("", true)
359
+ ]);
360
+ };
361
+ }
362
+ });
363
+ //#endregion
364
+ //#region src/vue/components/useLatestRequest.ts
365
+ function useLatestRequest() {
366
+ let counter = 0;
367
+ return {
368
+ begin() {
369
+ counter += 1;
370
+ return counter;
371
+ },
372
+ isCurrent(token) {
373
+ return token === counter;
374
+ }
375
+ };
376
+ }
377
+ //#endregion
378
+ //#region src/vue/components/DateRangePicker.vue?vue&type=script&setup=true&lang.ts
379
+ var _hoisted_1$13 = {
380
+ class: "flex flex-wrap items-end gap-2",
381
+ "data-testid": "accounting-daterange"
382
+ };
383
+ var _hoisted_2$12 = { class: "text-xs text-gray-500 flex flex-col gap-1" };
384
+ var _hoisted_3$12 = ["value"];
385
+ var _hoisted_4$12 = { value: "currentQuarter" };
386
+ var _hoisted_5$12 = { value: "previousQuarter" };
387
+ var _hoisted_6$12 = { value: "currentYear" };
388
+ var _hoisted_7$11 = { value: "previousYear" };
389
+ var _hoisted_8$11 = {
390
+ key: 0,
391
+ value: "lifetime"
392
+ };
393
+ var _hoisted_9$10 = { value: "all" };
394
+ var _hoisted_10$10 = { class: "text-xs text-gray-500 flex flex-col gap-1" };
395
+ var _hoisted_11$9 = ["value"];
396
+ var _hoisted_12$9 = { class: "text-xs text-gray-500 flex flex-col gap-1" };
397
+ var _hoisted_13$9 = ["value"];
398
+ //#endregion
399
+ //#region src/vue/components/DateRangePicker.vue
400
+ var DateRangePicker_default = /* @__PURE__ */ defineComponent({
401
+ __name: "DateRangePicker",
402
+ props: {
403
+ modelValue: {},
404
+ fiscalYearEnd: {},
405
+ openingDate: {}
406
+ },
407
+ emits: ["update:modelValue"],
408
+ setup(__props, { emit: __emit }) {
409
+ const { t } = useI18n();
410
+ const props = __props;
411
+ const emit = __emit;
412
+ const hasOpeningDate = computed(() => Boolean(props.openingDate));
413
+ const UNBOUNDED_RANGE = {
414
+ from: "",
415
+ to: ""
416
+ };
417
+ /** From the book's opening date through today. Hidden from the menu
418
+ * when the parent hasn't supplied an opening. */
419
+ function lifetimeRange() {
420
+ if (!props.openingDate) return null;
421
+ return {
422
+ from: props.openingDate,
423
+ to: localDateString()
424
+ };
425
+ }
426
+ function rangesEqual(left, right) {
427
+ return left.from === right.from && left.to === right.to;
428
+ }
429
+ const selectedShortcut = computed(() => {
430
+ const value = props.modelValue;
431
+ const today = /* @__PURE__ */ new Date();
432
+ if (rangesEqual(value, currentQuarterRange(props.fiscalYearEnd, today))) return "currentQuarter";
433
+ if (rangesEqual(value, previousQuarterRange(props.fiscalYearEnd, today))) return "previousQuarter";
434
+ if (rangesEqual(value, currentFiscalYearRange(props.fiscalYearEnd, today))) return "currentYear";
435
+ if (rangesEqual(value, previousFiscalYearRange(props.fiscalYearEnd, today))) return "previousYear";
436
+ const lifetime = lifetimeRange();
437
+ if (lifetime && rangesEqual(value, lifetime)) return "lifetime";
438
+ if (rangesEqual(value, UNBOUNDED_RANGE)) return "all";
439
+ return "";
440
+ });
441
+ function onShortcutChange(raw) {
442
+ const today = /* @__PURE__ */ new Date();
443
+ if (raw === "currentQuarter") emit("update:modelValue", currentQuarterRange(props.fiscalYearEnd, today));
444
+ else if (raw === "previousQuarter") emit("update:modelValue", previousQuarterRange(props.fiscalYearEnd, today));
445
+ else if (raw === "currentYear") emit("update:modelValue", currentFiscalYearRange(props.fiscalYearEnd, today));
446
+ else if (raw === "previousYear") emit("update:modelValue", previousFiscalYearRange(props.fiscalYearEnd, today));
447
+ else if (raw === "lifetime") {
448
+ const lifetime = lifetimeRange();
449
+ if (lifetime) emit("update:modelValue", lifetime);
450
+ } else if (raw === "all") emit("update:modelValue", UNBOUNDED_RANGE);
451
+ }
452
+ function onFromChange(value) {
453
+ emit("update:modelValue", {
454
+ from: value,
455
+ to: props.modelValue.to
456
+ });
457
+ }
458
+ function onToChange(value) {
459
+ emit("update:modelValue", {
460
+ from: props.modelValue.from,
461
+ to: value
462
+ });
463
+ }
464
+ return (_ctx, _cache) => {
465
+ return openBlock(), createElementBlock("div", _hoisted_1$13, [
466
+ createElementVNode("label", _hoisted_2$12, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.dateRange.shortcutLabel")) + " ", 1), createElementVNode("select", {
467
+ value: selectedShortcut.value,
468
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
469
+ "data-testid": "accounting-daterange-shortcut",
470
+ onChange: _cache[0] || (_cache[0] = ($event) => onShortcutChange($event.target.value))
471
+ }, [
472
+ _cache[3] || (_cache[3] = createElementVNode("option", {
473
+ value: "",
474
+ hidden: ""
475
+ }, null, -1)),
476
+ createElementVNode("option", _hoisted_4$12, toDisplayString(unref(t)("pluginAccounting.dateRange.currentQuarter")), 1),
477
+ createElementVNode("option", _hoisted_5$12, toDisplayString(unref(t)("pluginAccounting.dateRange.previousQuarter")), 1),
478
+ createElementVNode("option", _hoisted_6$12, toDisplayString(unref(t)("pluginAccounting.dateRange.currentYear")), 1),
479
+ createElementVNode("option", _hoisted_7$11, toDisplayString(unref(t)("pluginAccounting.dateRange.previousYear")), 1),
480
+ hasOpeningDate.value ? (openBlock(), createElementBlock("option", _hoisted_8$11, toDisplayString(unref(t)("pluginAccounting.dateRange.lifetime")), 1)) : createCommentVNode("", true),
481
+ createElementVNode("option", _hoisted_9$10, toDisplayString(unref(t)("pluginAccounting.dateRange.all")), 1)
482
+ ], 40, _hoisted_3$12)]),
483
+ createElementVNode("label", _hoisted_10$10, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.dateRange.fromLabel")) + " ", 1), createElementVNode("input", {
484
+ value: __props.modelValue.from,
485
+ type: "date",
486
+ class: "h-8 px-2 rounded border border-gray-300 text-sm",
487
+ "data-testid": "accounting-daterange-from",
488
+ onInput: _cache[1] || (_cache[1] = ($event) => onFromChange($event.target.value))
489
+ }, null, 40, _hoisted_11$9)]),
490
+ createElementVNode("label", _hoisted_12$9, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.dateRange.toLabel")) + " ", 1), createElementVNode("input", {
491
+ value: __props.modelValue.to,
492
+ type: "date",
493
+ class: "h-8 px-2 rounded border border-gray-300 text-sm",
494
+ "data-testid": "accounting-daterange-to",
495
+ onInput: _cache[2] || (_cache[2] = ($event) => onToChange($event.target.value))
496
+ }, null, 40, _hoisted_13$9)])
497
+ ]);
498
+ };
499
+ }
500
+ });
501
+ //#endregion
502
+ //#region src/vue/components/accountNumbering.ts
503
+ var ACCOUNT_TYPE_PREFIX = {
504
+ asset: 1,
505
+ liability: 2,
506
+ equity: 3,
507
+ income: 4,
508
+ expense: 5
509
+ };
510
+ var TAX_ACCOUNT_PREFIXES = ["14"];
511
+ /** Returns `true` for codes whose first two digits identify a
512
+ * tax-related current asset (`14xx`) — i.e. the input-tax /
513
+ * purchase side of consumption / sales / VAT bookkeeping. Drives
514
+ * Ledger column visibility and the JournalEntryForm per-line
515
+ * tax-registration ID input. Output-tax (24xx) is intentionally
516
+ * excluded: the counterparty's registration ID is only
517
+ * load-bearing for input-tax-credit eligibility on purchases. */
518
+ function isTaxAccountCode(code) {
519
+ return TAX_ACCOUNT_PREFIXES.some((prefix) => code.startsWith(prefix));
520
+ }
521
+ var ACCOUNT_CODE_RE = /^\d{4}$/;
522
+ var SUGGESTED_GAP = 10;
523
+ function isValidAccountCode(code) {
524
+ return ACCOUNT_CODE_RE.test(code);
525
+ }
526
+ function typeForCode(code) {
527
+ if (!isValidAccountCode(code)) return null;
528
+ const leading = Number.parseInt(code[0], 10);
529
+ for (const [type, prefix] of Object.entries(ACCOUNT_TYPE_PREFIX)) if (prefix === leading) return type;
530
+ return null;
531
+ }
532
+ function codeMatchesType(code, type) {
533
+ return typeForCode(code) === type;
534
+ }
535
+ /** Suggest the next free 4-digit code for `type`. Picks max-in-range
536
+ * + SUGGESTED_GAP so users keep room to insert sibling accounts
537
+ * later (the standard accounting convention). Falls back to the
538
+ * prefix base when the range is empty, and to max+1 when +gap would
539
+ * spill out of the 4-digit prefix window. */
540
+ function suggestNextCode(type, accounts) {
541
+ const prefix = ACCOUNT_TYPE_PREFIX[type];
542
+ const inRange = [];
543
+ for (const account of accounts) {
544
+ if (!isValidAccountCode(account.code)) continue;
545
+ const value = Number.parseInt(account.code, 10);
546
+ if (Math.floor(value / 1e3) !== prefix) continue;
547
+ inRange.push(value);
548
+ }
549
+ if (inRange.length === 0) return `${prefix}000`;
550
+ const max = Math.max(...inRange);
551
+ const candidate = max + SUGGESTED_GAP;
552
+ if (Math.floor(candidate / 1e3) === prefix && candidate <= 9999) return String(candidate);
553
+ const fallback = max + 1;
554
+ if (Math.floor(fallback / 1e3) === prefix && fallback <= 9999) return String(fallback);
555
+ return `${prefix}999`;
556
+ }
557
+ //#endregion
558
+ //#region src/vue/components/AccountRow.vue?vue&type=script&setup=true&lang.ts
559
+ var _hoisted_1$12 = ["data-testid"];
560
+ var _hoisted_2$11 = [
561
+ "checked",
562
+ "title",
563
+ "aria-label",
564
+ "data-testid"
565
+ ];
566
+ var _hoisted_3$11 = { class: "font-mono text-xs text-gray-500 w-16 shrink-0" };
567
+ var _hoisted_4$11 = ["data-testid"];
568
+ var _hoisted_5$11 = ["title"];
569
+ var _hoisted_6$11 = [
570
+ "data-testid",
571
+ "disabled",
572
+ "aria-hidden",
573
+ "tabindex"
574
+ ];
575
+ //#endregion
576
+ //#region src/vue/components/AccountRow.vue
577
+ var AccountRow_default = /* @__PURE__ */ defineComponent({
578
+ __name: "AccountRow",
579
+ props: { account: {} },
580
+ emits: ["edit", "toggleActive"],
581
+ setup(__props, { emit: __emit }) {
582
+ const { t } = useI18n();
583
+ const props = __props;
584
+ const emit = __emit;
585
+ const inactive = computed(() => props.account.active === false);
586
+ return (_ctx, _cache) => {
587
+ return openBlock(), createElementBlock("div", {
588
+ class: normalizeClass(["flex items-center gap-2 px-2 py-0.5 text-sm", inactive.value ? "opacity-60" : ""]),
589
+ "data-testid": `accounting-accounts-row-${__props.account.code}`
590
+ }, [
591
+ createElementVNode("input", {
592
+ type: "checkbox",
593
+ checked: !inactive.value,
594
+ title: inactive.value ? unref(t)("pluginAccounting.accounts.reactivate") : unref(t)("pluginAccounting.accounts.deactivate"),
595
+ "aria-label": inactive.value ? unref(t)("pluginAccounting.accounts.reactivate") : unref(t)("pluginAccounting.accounts.deactivate"),
596
+ class: "h-4 w-4 shrink-0 cursor-pointer",
597
+ "data-testid": `accounting-accounts-toggle-${__props.account.code}`,
598
+ onChange: _cache[0] || (_cache[0] = ($event) => emit("toggleActive"))
599
+ }, null, 40, _hoisted_2$11),
600
+ createElementVNode("span", _hoisted_3$11, toDisplayString(__props.account.code), 1),
601
+ createElementVNode("span", {
602
+ class: normalizeClass(["grow min-w-0 truncate", inactive.value ? "line-through" : ""]),
603
+ "data-testid": inactive.value ? `accounting-accounts-inactive-${__props.account.code}` : void 0
604
+ }, toDisplayString(__props.account.name), 11, _hoisted_4$11),
605
+ __props.account.note ? (openBlock(), createElementBlock("span", {
606
+ key: 0,
607
+ class: "text-xs text-gray-400 truncate max-w-[8rem]",
608
+ title: __props.account.note
609
+ }, toDisplayString(__props.account.note), 9, _hoisted_5$11)) : createCommentVNode("", true),
610
+ createElementVNode("button", {
611
+ type: "button",
612
+ class: normalizeClass(["h-8 px-2.5 rounded text-sm text-blue-600 hover:bg-blue-50", inactive.value ? "invisible" : ""]),
613
+ "data-testid": `accounting-accounts-edit-${__props.account.code}`,
614
+ disabled: inactive.value,
615
+ "aria-hidden": inactive.value ? "true" : void 0,
616
+ tabindex: inactive.value ? -1 : void 0,
617
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("edit"))
618
+ }, toDisplayString(unref(t)("pluginAccounting.accounts.edit")), 11, _hoisted_6$11)
619
+ ], 10, _hoisted_1$12);
620
+ };
621
+ }
622
+ });
623
+ /**
624
+ * Validate just the code field. Split out from the full draft
625
+ * validator so AccountEditor can paint a per-field red border in
626
+ * realtime without re-running the name check on every keystroke.
627
+ */
628
+ function validateCodeField(draft, existing, isNew) {
629
+ const trimmedCode = draft.code.trim();
630
+ if (trimmedCode.length === 0) return "emptyCode";
631
+ if (trimmedCode.startsWith("_")) return "reservedCode";
632
+ if (isNew && !isValidAccountCode(trimmedCode)) return "invalidCodeFormat";
633
+ if (isNew && !codeMatchesType(trimmedCode, draft.type)) return "codeTypeMismatch";
634
+ if (isNew && existing.some((account) => account.code === trimmedCode)) return "duplicateCode";
635
+ return null;
636
+ }
637
+ /**
638
+ * Validate just the name field. Empty + duplicate (case-insensitive,
639
+ * trimmed) against other accounts. On edit, the account being edited
640
+ * is excluded from the duplicate check via `draft.code` — otherwise
641
+ * every save would flag the user's own row as a collision.
642
+ */
643
+ function validateNameField(draft, existing, isNew) {
644
+ const trimmedName = draft.name.trim();
645
+ if (trimmedName.length === 0) return "emptyName";
646
+ const folded = trimmedName.toLowerCase();
647
+ if (existing.some((account) => {
648
+ if (!isNew && account.code === draft.code.trim()) return false;
649
+ return account.name.trim().toLowerCase() === folded;
650
+ })) return "duplicateName";
651
+ return null;
652
+ }
653
+ /**
654
+ * Validate a draft about to be sent to `upsertAccount`. Returns
655
+ * `null` on success or an error code on failure. Caller maps the
656
+ * code to a localized message.
657
+ *
658
+ * `existing` is the current chart of accounts — used to detect a
659
+ * duplicate code on a brand-new entry (otherwise the server would
660
+ * silently overwrite the existing account, which is rarely what
661
+ * the user typing into the "Add account" form intended).
662
+ *
663
+ * Code errors take precedence over name errors so the user fixes
664
+ * one stable issue at a time as they type.
665
+ */
666
+ function validateAccountDraft(draft, existing, isNew) {
667
+ return validateCodeField(draft, existing, isNew) ?? validateNameField(draft, existing, isNew);
668
+ }
669
+ //#endregion
670
+ //#region src/vue/components/AccountEditor.vue?vue&type=script&setup=true&lang.ts
671
+ var _hoisted_1$11 = ["data-testid"];
672
+ var _hoisted_2$10 = { class: "flex flex-wrap gap-2" };
673
+ var _hoisted_3$10 = { class: "text-xs text-gray-500 flex flex-col gap-1 w-28" };
674
+ var _hoisted_4$10 = {
675
+ class: "px-2 flex items-center bg-gray-100 text-gray-500 border-r border-gray-200 select-none",
676
+ "data-testid": "accounting-accounts-form-code-prefix"
677
+ };
678
+ var _hoisted_5$10 = { class: "text-xs text-gray-500 flex flex-col gap-1 grow min-w-[10rem]" };
679
+ var _hoisted_6$10 = { class: "text-xs text-gray-500 flex flex-col gap-1 w-32" };
680
+ var _hoisted_7$10 = ["value"];
681
+ var _hoisted_8$10 = { class: "text-xs text-gray-500 flex flex-col gap-1" };
682
+ var _hoisted_9$9 = { class: "text-gray-400" };
683
+ var _hoisted_10$9 = {
684
+ key: 0,
685
+ class: "text-xs text-gray-400"
686
+ };
687
+ var _hoisted_11$8 = {
688
+ class: "text-xs text-red-500 min-h-[1rem]",
689
+ "data-testid": "accounting-accounts-form-error"
690
+ };
691
+ var _hoisted_12$8 = { class: "flex justify-end gap-2" };
692
+ var _hoisted_13$8 = ["disabled"];
693
+ //#endregion
694
+ //#region src/vue/components/AccountEditor.vue
695
+ var AccountEditor_default = /* @__PURE__ */ defineComponent({
696
+ __name: "AccountEditor",
697
+ props: {
698
+ draft: {},
699
+ isNew: { type: Boolean },
700
+ busy: { type: Boolean },
701
+ error: {},
702
+ existingAccounts: {}
703
+ },
704
+ emits: ["save", "cancel"],
705
+ setup(__props, { emit: __emit }) {
706
+ const { t } = useI18n();
707
+ const props = __props;
708
+ const emit = __emit;
709
+ const TYPE_OPTIONS = [
710
+ "asset",
711
+ "liability",
712
+ "equity",
713
+ "income",
714
+ "expense"
715
+ ];
716
+ const VALIDATION_MESSAGE_KEYS = {
717
+ emptyCode: "pluginAccounting.accounts.errorEmptyCode",
718
+ reservedCode: "pluginAccounting.accounts.errorReservedCode",
719
+ invalidCodeFormat: "pluginAccounting.accounts.errorInvalidCodeFormat",
720
+ codeTypeMismatch: "pluginAccounting.accounts.errorCodeTypeMismatch",
721
+ emptyName: "pluginAccounting.accounts.errorEmptyName",
722
+ duplicateCode: "pluginAccounting.accounts.errorDuplicateCode",
723
+ duplicateName: "pluginAccounting.accounts.errorDuplicateName"
724
+ };
725
+ const local = reactive({ ...props.draft });
726
+ const nameInput = ref(null);
727
+ const codeTouched = ref(false);
728
+ const nameTouched = ref(false);
729
+ const codePrefix = computed(() => String(ACCOUNT_TYPE_PREFIX[local.type]));
730
+ const codeTrailing = computed({
731
+ get: () => {
732
+ const { code } = local;
733
+ if (code.startsWith(codePrefix.value)) return code.slice(codePrefix.value.length);
734
+ return code;
735
+ },
736
+ set: (val) => {
737
+ const cleaned = val.replace(/\D/g, "").slice(0, 3);
738
+ local.code = codePrefix.value + cleaned;
739
+ }
740
+ });
741
+ const codeError = computed(() => {
742
+ const result = validateCodeField(local, props.existingAccounts, props.isNew);
743
+ if (result === "emptyCode" && !codeTouched.value) return null;
744
+ return result;
745
+ });
746
+ const nameError = computed(() => {
747
+ const result = validateNameField(local, props.existingAccounts, props.isNew);
748
+ if (result === "emptyName" && !nameTouched.value && !props.isNew) return null;
749
+ return result;
750
+ });
751
+ const fieldErrorMessage = computed(() => {
752
+ const code = codeError.value;
753
+ if (code !== null) return t(VALIDATION_MESSAGE_KEYS[code]);
754
+ const name = nameError.value;
755
+ if (name !== null) return t(VALIDATION_MESSAGE_KEYS[name]);
756
+ return null;
757
+ });
758
+ watch(() => props.draft, (next) => {
759
+ local.code = next.code;
760
+ local.name = next.name;
761
+ local.type = next.type;
762
+ local.note = next.note;
763
+ codeTouched.value = false;
764
+ nameTouched.value = false;
765
+ });
766
+ onMounted(() => {
767
+ nextTick(() => nameInput.value?.focus());
768
+ });
769
+ function onSubmit() {
770
+ codeTouched.value = true;
771
+ nameTouched.value = true;
772
+ emit("save", {
773
+ code: local.code,
774
+ name: local.name,
775
+ type: local.type,
776
+ note: local.note
777
+ });
778
+ }
779
+ return (_ctx, _cache) => {
780
+ return openBlock(), createElementBlock("form", {
781
+ class: "flex flex-col gap-2 p-2 border border-blue-200 bg-blue-50/40 rounded text-sm",
782
+ "data-testid": __props.isNew ? "accounting-accounts-form-new" : `accounting-accounts-form-edit-${__props.draft.code}`,
783
+ onSubmit: withModifiers(onSubmit, ["prevent"])
784
+ }, [
785
+ createElementVNode("div", _hoisted_2$10, [
786
+ createElementVNode("label", _hoisted_3$10, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.accounts.columnCode")) + " ", 1), __props.isNew ? (openBlock(), createElementBlock("div", {
787
+ key: 0,
788
+ class: normalizeClass(["flex items-stretch h-8 rounded border bg-white text-sm font-mono overflow-hidden", codeError.value ? "border-red-500 ring-1 ring-red-500" : "border-gray-300 focus-within:ring-1 focus-within:ring-blue-500"])
789
+ }, [createElementVNode("span", _hoisted_4$10, toDisplayString(codePrefix.value), 1), withDirectives(createElementVNode("input", {
790
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => codeTrailing.value = $event),
791
+ type: "text",
792
+ inputmode: "numeric",
793
+ maxlength: "3",
794
+ pattern: "\\d{3}",
795
+ class: "px-2 grow w-0 outline-none bg-transparent",
796
+ "data-testid": "accounting-accounts-form-code",
797
+ onInput: _cache[1] || (_cache[1] = ($event) => codeTouched.value = true)
798
+ }, null, 544), [[vModelText, codeTrailing.value]])], 2)) : withDirectives((openBlock(), createElementBlock("input", {
799
+ key: 1,
800
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => local.code = $event),
801
+ type: "text",
802
+ disabled: "",
803
+ class: "h-8 px-2 rounded border border-gray-300 text-sm font-mono bg-gray-100 text-gray-500",
804
+ "data-testid": "accounting-accounts-form-code"
805
+ }, null, 512)), [[vModelText, local.code]])]),
806
+ createElementVNode("label", _hoisted_5$10, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.accounts.columnName")) + " ", 1), withDirectives(createElementVNode("input", {
807
+ ref_key: "nameInput",
808
+ ref: nameInput,
809
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => local.name = $event),
810
+ type: "text",
811
+ class: normalizeClass(["h-8 px-2 rounded border text-sm focus:outline-none", nameError.value ? "border-red-500 ring-1 ring-red-500" : "border-gray-300 focus:ring-1 focus:ring-blue-500"]),
812
+ "data-testid": "accounting-accounts-form-name",
813
+ onInput: _cache[4] || (_cache[4] = ($event) => nameTouched.value = true)
814
+ }, null, 34), [[vModelText, local.name]])]),
815
+ createElementVNode("label", _hoisted_6$10, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.accounts.columnType")) + " ", 1), withDirectives(createElementVNode("select", {
816
+ "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => local.type = $event),
817
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white disabled:bg-gray-100 disabled:text-gray-500",
818
+ disabled: "",
819
+ "data-testid": "accounting-accounts-form-type"
820
+ }, [(openBlock(), createElementBlock(Fragment, null, renderList(TYPE_OPTIONS, (option) => {
821
+ return createElementVNode("option", {
822
+ key: option,
823
+ value: option
824
+ }, toDisplayString(unref(t)(`pluginAccounting.accounts.typeOption.${option}`)), 9, _hoisted_7$10);
825
+ }), 64))], 512), [[vModelSelect, local.type]])])
826
+ ]),
827
+ createElementVNode("label", _hoisted_8$10, [createElementVNode("span", null, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.accounts.columnNote")) + " ", 1), createElementVNode("span", _hoisted_9$9, toDisplayString(unref(t)("pluginAccounting.accounts.noteOptional")), 1)]), withDirectives(createElementVNode("input", {
828
+ "onUpdate:modelValue": _cache[6] || (_cache[6] = ($event) => local.note = $event),
829
+ type: "text",
830
+ class: "h-8 px-2 rounded border border-gray-300 text-sm",
831
+ "data-testid": "accounting-accounts-form-note"
832
+ }, null, 512), [[vModelText, local.note]])]),
833
+ !__props.isNew ? (openBlock(), createElementBlock("p", _hoisted_10$9, toDisplayString(unref(t)("pluginAccounting.accounts.codeReadOnlyHint")), 1)) : createCommentVNode("", true),
834
+ createElementVNode("p", _hoisted_11$8, toDisplayString(fieldErrorMessage.value ?? __props.error ?? ""), 1),
835
+ createElementVNode("div", _hoisted_12$8, [createElementVNode("button", {
836
+ type: "button",
837
+ class: "h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50",
838
+ "data-testid": "accounting-accounts-form-cancel",
839
+ onClick: _cache[7] || (_cache[7] = ($event) => emit("cancel"))
840
+ }, toDisplayString(unref(t)("pluginAccounting.accounts.cancel")), 1), createElementVNode("button", {
841
+ type: "submit",
842
+ class: "h-8 px-2.5 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50",
843
+ disabled: __props.busy,
844
+ "data-testid": "accounting-accounts-form-save"
845
+ }, toDisplayString(__props.busy ? unref(t)("pluginAccounting.accounts.saving") : unref(t)("pluginAccounting.accounts.save")), 9, _hoisted_13$8)])
846
+ ], 40, _hoisted_1$11);
847
+ };
848
+ }
849
+ });
850
+ //#endregion
851
+ //#region src/vue/components/AccountsModal.vue?vue&type=script&setup=true&lang.ts
852
+ var _hoisted_1$10 = { class: "bg-white rounded shadow-lg w-[32rem] max-h-[80vh] flex flex-col" };
853
+ var _hoisted_2$9 = { class: "flex items-center justify-between px-4 py-2 border-b border-gray-200 shrink-0" };
854
+ var _hoisted_3$9 = {
855
+ id: "accounting-accounts-modal-title",
856
+ class: "text-base font-semibold"
857
+ };
858
+ var _hoisted_4$9 = ["aria-label"];
859
+ var _hoisted_5$9 = { class: "flex-1 overflow-auto px-4 py-3 flex flex-col gap-3" };
860
+ var _hoisted_6$9 = {
861
+ key: 0,
862
+ class: "text-xs text-green-600",
863
+ "data-testid": "accounting-accounts-success"
864
+ };
865
+ var _hoisted_7$9 = {
866
+ key: 1,
867
+ class: "text-xs text-red-500",
868
+ "data-testid": "accounting-accounts-toggle-error"
869
+ };
870
+ var _hoisted_8$9 = { class: "text-xs font-semibold text-gray-500 uppercase tracking-wide" };
871
+ var _hoisted_9$8 = {
872
+ key: 0,
873
+ class: "text-xs text-gray-400 italic px-1"
874
+ };
875
+ var _hoisted_10$8 = ["data-testid", "onClick"];
876
+ var SUCCESS_FADE_MS = 2500;
877
+ //#endregion
878
+ //#region src/vue/components/AccountsModal.vue
879
+ var AccountsModal_default = /* @__PURE__ */ defineComponent({
880
+ __name: "AccountsModal",
881
+ props: {
882
+ bookId: {},
883
+ accounts: {}
884
+ },
885
+ emits: ["close", "changed"],
886
+ setup(__props, { emit: __emit }) {
887
+ const { t } = useI18n();
888
+ const props = __props;
889
+ const emit = __emit;
890
+ const ACCOUNT_TYPES = [
891
+ "asset",
892
+ "liability",
893
+ "equity",
894
+ "income",
895
+ "expense"
896
+ ];
897
+ const VALIDATION_MESSAGE_KEYS = {
898
+ emptyCode: "pluginAccounting.accounts.errorEmptyCode",
899
+ reservedCode: "pluginAccounting.accounts.errorReservedCode",
900
+ invalidCodeFormat: "pluginAccounting.accounts.errorInvalidCodeFormat",
901
+ codeTypeMismatch: "pluginAccounting.accounts.errorCodeTypeMismatch",
902
+ emptyName: "pluginAccounting.accounts.errorEmptyName",
903
+ duplicateCode: "pluginAccounting.accounts.errorDuplicateCode",
904
+ duplicateName: "pluginAccounting.accounts.errorDuplicateName"
905
+ };
906
+ const groups = computed(() => ACCOUNT_TYPES.map((type) => ({
907
+ type,
908
+ accounts: props.accounts.filter((account) => account.type === type).slice().sort(byCode)
909
+ })));
910
+ function byCode(left, right) {
911
+ return left.code.localeCompare(right.code);
912
+ }
913
+ const editingCode = ref(null);
914
+ const addingNew = ref(false);
915
+ const draft = ref(emptyDraft("asset"));
916
+ const saving = ref(false);
917
+ const error = ref(null);
918
+ const toggleSaving = ref(false);
919
+ const toggleError = ref(null);
920
+ const successMessage = ref(null);
921
+ const closeButton = ref(null);
922
+ const newEditorWrapper = ref(null);
923
+ let successTimer = null;
924
+ function emptyDraft(type) {
925
+ return {
926
+ code: "",
927
+ name: "",
928
+ type,
929
+ note: ""
930
+ };
931
+ }
932
+ function draftForNew(type) {
933
+ return {
934
+ code: suggestNextCode(type, props.accounts),
935
+ name: "",
936
+ type,
937
+ note: ""
938
+ };
939
+ }
940
+ function bindNewEditor(node, sectionType) {
941
+ if (sectionType !== draft.value.type) return;
942
+ newEditorWrapper.value = node ?? null;
943
+ }
944
+ function onEdit(account) {
945
+ addingNew.value = false;
946
+ error.value = null;
947
+ draft.value = {
948
+ code: account.code,
949
+ name: account.name,
950
+ type: account.type,
951
+ note: account.note ?? ""
952
+ };
953
+ editingCode.value = account.code;
954
+ }
955
+ function onAdd(type) {
956
+ editingCode.value = null;
957
+ error.value = null;
958
+ draft.value = draftForNew(type);
959
+ addingNew.value = true;
960
+ nextTick(() => {
961
+ newEditorWrapper.value?.scrollIntoView({
962
+ behavior: "smooth",
963
+ block: "nearest"
964
+ });
965
+ });
966
+ }
967
+ function onCancelEditor() {
968
+ editingCode.value = null;
969
+ addingNew.value = false;
970
+ error.value = null;
971
+ draft.value = emptyDraft("asset");
972
+ }
973
+ function validateDraft(next, isNew) {
974
+ const code = validateAccountDraft(next, props.accounts, isNew);
975
+ return code === null ? null : t(VALIDATION_MESSAGE_KEYS[code]);
976
+ }
977
+ async function onSave(next) {
978
+ if (saving.value) return;
979
+ const isNew = addingNew.value;
980
+ const validation = validateDraft(next, isNew);
981
+ if (validation !== null) {
982
+ error.value = validation;
983
+ return;
984
+ }
985
+ saving.value = true;
986
+ error.value = null;
987
+ try {
988
+ const account = {
989
+ code: next.code.trim(),
990
+ name: next.name.trim(),
991
+ type: next.type
992
+ };
993
+ const note = next.note.trim();
994
+ if (note.length > 0) account.note = note;
995
+ if (!isNew) {
996
+ if (props.accounts.find((entry) => entry.code === account.code)?.active === false) account.active = false;
997
+ }
998
+ const result = await upsertAccount(account, props.bookId);
999
+ if (!result.ok) {
1000
+ error.value = result.error;
1001
+ return;
1002
+ }
1003
+ onCancelEditor();
1004
+ showSuccess(t("pluginAccounting.accounts.success"));
1005
+ emit("changed");
1006
+ } catch (err) {
1007
+ error.value = errorMessage(err);
1008
+ } finally {
1009
+ saving.value = false;
1010
+ }
1011
+ }
1012
+ async function onToggleActive(account) {
1013
+ if (toggleSaving.value) return;
1014
+ onCancelEditor();
1015
+ const willDeactivate = account.active !== false;
1016
+ toggleSaving.value = true;
1017
+ toggleError.value = null;
1018
+ try {
1019
+ const next = {
1020
+ code: account.code,
1021
+ name: account.name,
1022
+ type: account.type
1023
+ };
1024
+ if (account.note !== void 0 && account.note.length > 0) next.note = account.note;
1025
+ next.active = !willDeactivate;
1026
+ const result = await upsertAccount(next, props.bookId);
1027
+ if (!result.ok) {
1028
+ toggleError.value = result.error;
1029
+ return;
1030
+ }
1031
+ emit("changed");
1032
+ } catch (err) {
1033
+ toggleError.value = errorMessage(err);
1034
+ } finally {
1035
+ toggleSaving.value = false;
1036
+ }
1037
+ }
1038
+ function showSuccess(message) {
1039
+ successMessage.value = message;
1040
+ if (successTimer !== null) clearTimeout(successTimer);
1041
+ successTimer = setTimeout(() => {
1042
+ successMessage.value = null;
1043
+ successTimer = null;
1044
+ }, SUCCESS_FADE_MS);
1045
+ }
1046
+ function onBackdropClick() {
1047
+ emit("close");
1048
+ }
1049
+ onMounted(() => {
1050
+ nextTick(() => closeButton.value?.focus());
1051
+ });
1052
+ onUnmounted(() => {
1053
+ if (successTimer !== null) clearTimeout(successTimer);
1054
+ });
1055
+ return (_ctx, _cache) => {
1056
+ return openBlock(), createElementBlock("div", {
1057
+ class: "fixed inset-0 z-50 bg-black/20 flex items-center justify-center",
1058
+ role: "dialog",
1059
+ "aria-modal": "true",
1060
+ "aria-labelledby": "accounting-accounts-modal-title",
1061
+ "data-testid": "accounting-accounts-modal",
1062
+ onClick: withModifiers(onBackdropClick, ["self"]),
1063
+ onKeydown: _cache[1] || (_cache[1] = withKeys(($event) => emit("close"), ["esc"]))
1064
+ }, [createElementVNode("div", _hoisted_1$10, [createElementVNode("header", _hoisted_2$9, [createElementVNode("h3", _hoisted_3$9, toDisplayString(unref(t)("pluginAccounting.accounts.modalTitle")), 1), createElementVNode("button", {
1065
+ ref_key: "closeButton",
1066
+ ref: closeButton,
1067
+ type: "button",
1068
+ class: "h-8 w-8 flex items-center justify-center rounded text-gray-500 hover:bg-gray-100",
1069
+ "data-testid": "accounting-accounts-close",
1070
+ "aria-label": unref(t)("pluginAccounting.common.cancel"),
1071
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("close"))
1072
+ }, [..._cache[2] || (_cache[2] = [createElementVNode("span", { class: "material-icons text-base" }, "close", -1)])], 8, _hoisted_4$9)]), createElementVNode("div", _hoisted_5$9, [
1073
+ successMessage.value ? (openBlock(), createElementBlock("p", _hoisted_6$9, toDisplayString(successMessage.value), 1)) : createCommentVNode("", true),
1074
+ toggleError.value ? (openBlock(), createElementBlock("p", _hoisted_7$9, toDisplayString(toggleError.value), 1)) : createCommentVNode("", true),
1075
+ (openBlock(true), createElementBlock(Fragment, null, renderList(groups.value, (group) => {
1076
+ return openBlock(), createElementBlock("section", {
1077
+ key: group.type,
1078
+ class: "flex flex-col gap-1"
1079
+ }, [
1080
+ createElementVNode("h4", _hoisted_8$9, toDisplayString(unref(t)(`pluginAccounting.accounts.sectionTitle.${group.type}`)), 1),
1081
+ group.accounts.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_9$8, toDisplayString(unref(t)("pluginAccounting.common.empty")), 1)) : createCommentVNode("", true),
1082
+ (openBlock(true), createElementBlock(Fragment, null, renderList(group.accounts, (account) => {
1083
+ return openBlock(), createElementBlock(Fragment, { key: account.code }, [editingCode.value !== account.code ? (openBlock(), createBlock(AccountRow_default, {
1084
+ key: 0,
1085
+ account,
1086
+ onEdit: ($event) => onEdit(account),
1087
+ onToggleActive: ($event) => onToggleActive(account)
1088
+ }, null, 8, [
1089
+ "account",
1090
+ "onEdit",
1091
+ "onToggleActive"
1092
+ ])) : (openBlock(), createBlock(AccountEditor_default, {
1093
+ key: 1,
1094
+ draft: draft.value,
1095
+ "is-new": false,
1096
+ busy: saving.value,
1097
+ error: error.value,
1098
+ "existing-accounts": __props.accounts,
1099
+ onSave,
1100
+ onCancel: onCancelEditor
1101
+ }, null, 8, [
1102
+ "draft",
1103
+ "busy",
1104
+ "error",
1105
+ "existing-accounts"
1106
+ ]))], 64);
1107
+ }), 128)),
1108
+ addingNew.value && draft.value.type === group.type ? (openBlock(), createElementBlock("div", {
1109
+ key: 1,
1110
+ ref_for: true,
1111
+ ref: (node) => bindNewEditor(node, group.type)
1112
+ }, [createVNode(AccountEditor_default, {
1113
+ draft: draft.value,
1114
+ "is-new": "",
1115
+ busy: saving.value,
1116
+ error: error.value,
1117
+ "existing-accounts": __props.accounts,
1118
+ onSave,
1119
+ onCancel: onCancelEditor
1120
+ }, null, 8, [
1121
+ "draft",
1122
+ "busy",
1123
+ "error",
1124
+ "existing-accounts"
1125
+ ])], 512)) : (openBlock(), createElementBlock("button", {
1126
+ key: 2,
1127
+ type: "button",
1128
+ class: "self-start h-8 px-2.5 flex items-center gap-1 rounded text-xs text-gray-600 hover:bg-gray-100",
1129
+ "data-testid": `accounting-accounts-add-${group.type}`,
1130
+ onClick: ($event) => onAdd(group.type)
1131
+ }, [_cache[3] || (_cache[3] = createElementVNode("span", { class: "material-icons text-sm" }, "add", -1)), createElementVNode("span", null, toDisplayString(unref(t)("pluginAccounting.accounts.addToCategory", { type: unref(t)(`pluginAccounting.accounts.typeOption.${group.type}`) })), 1)], 8, _hoisted_10$8))
1132
+ ]);
1133
+ }), 128))
1134
+ ])])], 32);
1135
+ };
1136
+ }
1137
+ });
1138
+ //#endregion
1139
+ //#region src/vue/components/JournalEntryForm.vue?vue&type=script&setup=true&lang.ts
1140
+ var _hoisted_1$9 = {
1141
+ key: 0,
1142
+ class: "text-base font-semibold"
1143
+ };
1144
+ var _hoisted_2$8 = { class: "flex flex-wrap gap-3" };
1145
+ var _hoisted_3$8 = { class: "text-xs text-gray-500 flex flex-col gap-1" };
1146
+ var _hoisted_4$8 = { class: "text-xs text-gray-500 flex flex-col gap-1 grow min-w-0" };
1147
+ var _hoisted_5$8 = { class: "w-full text-sm" };
1148
+ var _hoisted_6$8 = { class: "text-xs text-gray-500 border-b border-gray-200" };
1149
+ var _hoisted_7$8 = { class: "text-left py-1 px-2" };
1150
+ var _hoisted_8$8 = { class: "text-right py-1 px-2 w-32" };
1151
+ var _hoisted_9$7 = { class: "text-right py-1 px-2 w-32" };
1152
+ var _hoisted_10$7 = {
1153
+ key: 0,
1154
+ class: "text-left py-1 px-2 w-40"
1155
+ };
1156
+ var _hoisted_11$7 = { class: "py-1 px-2" };
1157
+ var _hoisted_12$7 = ["onUpdate:modelValue", "data-testid"];
1158
+ var _hoisted_13$7 = ["value"];
1159
+ var _hoisted_14$7 = { class: "py-1 px-2" };
1160
+ var _hoisted_15$6 = [
1161
+ "onUpdate:modelValue",
1162
+ "step",
1163
+ "data-testid",
1164
+ "onInput"
1165
+ ];
1166
+ var _hoisted_16$6 = { class: "py-1 px-2" };
1167
+ var _hoisted_17$6 = [
1168
+ "onUpdate:modelValue",
1169
+ "step",
1170
+ "data-testid",
1171
+ "onInput"
1172
+ ];
1173
+ var _hoisted_18$6 = {
1174
+ key: 0,
1175
+ class: "py-1 px-2"
1176
+ };
1177
+ var _hoisted_19$6 = [
1178
+ "onUpdate:modelValue",
1179
+ "placeholder",
1180
+ "data-testid",
1181
+ "aria-describedby"
1182
+ ];
1183
+ var _hoisted_20$6 = ["id", "data-testid"];
1184
+ var _hoisted_21$6 = { class: "py-1 px-2 text-right" };
1185
+ var _hoisted_22$5 = ["onClick"];
1186
+ var _hoisted_23$5 = { class: "flex items-center justify-between" };
1187
+ var _hoisted_24$5 = { class: "flex items-center gap-2" };
1188
+ var _hoisted_25$5 = {
1189
+ key: 1,
1190
+ class: "text-xs text-red-500",
1191
+ "data-testid": "accounting-entry-error"
1192
+ };
1193
+ var _hoisted_26$4 = {
1194
+ key: 2,
1195
+ class: "text-xs text-green-600",
1196
+ "data-testid": "accounting-entry-success"
1197
+ };
1198
+ var _hoisted_27$3 = { class: "flex items-center justify-between gap-2" };
1199
+ var _hoisted_28$2 = {
1200
+ key: 0,
1201
+ class: "text-xs text-gray-500 flex-1 min-w-0",
1202
+ "data-testid": "accounting-entry-edit-banner"
1203
+ };
1204
+ var _hoisted_29$2 = { key: 1 };
1205
+ var _hoisted_30$2 = { class: "flex items-center gap-2" };
1206
+ var _hoisted_31$2 = ["disabled"];
1207
+ var _hoisted_32$2 = ["disabled"];
1208
+ var DASH$1 = "—";
1209
+ var MAX_TAX_REGISTRATION_ID_LENGTH = 32;
1210
+ var JournalEntryForm_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
1211
+ __name: "JournalEntryForm",
1212
+ props: {
1213
+ bookId: {},
1214
+ accounts: {},
1215
+ currency: {},
1216
+ country: {},
1217
+ entryToEdit: {}
1218
+ },
1219
+ emits: ["submitted", "cancel"],
1220
+ setup(__props, { emit: __emit }) {
1221
+ const { t } = useI18n();
1222
+ const props = __props;
1223
+ const emit = __emit;
1224
+ const showAccountsModal = ref(false);
1225
+ function formatAccountLabel(account) {
1226
+ return `${account.name} (${account.code})`;
1227
+ }
1228
+ const selectableAccounts = computed(() => props.accounts.filter((account) => account.active !== false));
1229
+ const selectableAccountCodes = computed(() => new Set(selectableAccounts.value.map((account) => account.code)));
1230
+ function blankLine() {
1231
+ return {
1232
+ accountCode: "",
1233
+ debit: null,
1234
+ credit: null,
1235
+ taxRegistrationId: ""
1236
+ };
1237
+ }
1238
+ function isTaxRegistrationIdInvalid(line) {
1239
+ return line.taxRegistrationId.trim().length > MAX_TAX_REGISTRATION_ID_LENGTH;
1240
+ }
1241
+ function isTaxLine(line) {
1242
+ return line.accountCode !== "" && isTaxAccountCode(line.accountCode);
1243
+ }
1244
+ function isTaxRegistrationIdMissing(line) {
1245
+ if (!isTaxLine(line)) return false;
1246
+ if (!isPostable(line)) return false;
1247
+ if (!countryHasFeature("warnMissingTaxRegistrationId", props.country)) return false;
1248
+ return line.taxRegistrationId.trim() === "";
1249
+ }
1250
+ const date = ref(localDateString());
1251
+ const memo = ref("");
1252
+ const lines = ref([blankLine(), blankLine()]);
1253
+ const submitting = ref(false);
1254
+ const error = ref(null);
1255
+ const successMessage = ref(null);
1256
+ const isEditing = computed(() => Boolean(props.entryToEdit));
1257
+ const submitButtonLabel = computed(() => {
1258
+ if (submitting.value) return isEditing.value ? t("pluginAccounting.entryForm.updating") : t("pluginAccounting.entryForm.submitting");
1259
+ return isEditing.value ? t("pluginAccounting.entryForm.update") : t("pluginAccounting.entryForm.submit");
1260
+ });
1261
+ const editAttempted = ref(false);
1262
+ const editLocked = computed(() => isEditing.value && editAttempted.value);
1263
+ function addLine() {
1264
+ lines.value.push(blankLine());
1265
+ }
1266
+ function onDebitInput(line) {
1267
+ if (line.debit !== null && line.debit !== 0) line.credit = null;
1268
+ }
1269
+ function onCreditInput(line) {
1270
+ if (line.credit !== null && line.credit !== 0) line.debit = null;
1271
+ }
1272
+ const imbalance = computed(() => {
1273
+ let sum = 0;
1274
+ for (const line of lines.value) {
1275
+ if (!isPostable(line)) continue;
1276
+ if (isPositiveAmount(line.debit)) sum += line.debit;
1277
+ if (isPositiveAmount(line.credit)) sum -= line.credit;
1278
+ }
1279
+ return sum;
1280
+ });
1281
+ const hasAtLeastTwoPostableLines = computed(() => {
1282
+ let count = 0;
1283
+ for (const line of lines.value) {
1284
+ if (!isPostable(line)) continue;
1285
+ count += 1;
1286
+ if (count >= 2) return true;
1287
+ }
1288
+ return false;
1289
+ });
1290
+ const anyTaxLine = computed(() => lines.value.some(isTaxLine));
1291
+ const hasTaxRegistrationIdError = computed(() => lines.value.some(isTaxRegistrationIdInvalid));
1292
+ const balanced = computed(() => Math.abs(imbalance.value) <= .005 && hasAtLeastTwoPostableLines.value && !hasTaxRegistrationIdError.value);
1293
+ const imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));
1294
+ const step = computed(() => inputStepFor(props.currency));
1295
+ function isPositiveAmount(value) {
1296
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
1297
+ }
1298
+ function isPostable(line) {
1299
+ if (!line.accountCode) return false;
1300
+ if (!selectableAccountCodes.value.has(line.accountCode)) return false;
1301
+ return isPositiveAmount(line.debit) || isPositiveAmount(line.credit);
1302
+ }
1303
+ function toApiLines() {
1304
+ const out = [];
1305
+ for (const line of lines.value) {
1306
+ if (!isPostable(line)) continue;
1307
+ const apiLine = { accountCode: line.accountCode };
1308
+ if (isPositiveAmount(line.debit)) apiLine.debit = line.debit;
1309
+ if (isPositiveAmount(line.credit)) apiLine.credit = line.credit;
1310
+ if (isTaxLine(line)) {
1311
+ const trimmedTaxId = line.taxRegistrationId.trim();
1312
+ if (trimmedTaxId !== "") apiLine.taxRegistrationId = trimmedTaxId;
1313
+ }
1314
+ out.push(apiLine);
1315
+ }
1316
+ return out;
1317
+ }
1318
+ async function onSubmit() {
1319
+ if (submitting.value || !balanced.value || editLocked.value) return;
1320
+ submitting.value = true;
1321
+ error.value = null;
1322
+ successMessage.value = null;
1323
+ try {
1324
+ const editingId = props.entryToEdit?.id;
1325
+ if (editingId) {
1326
+ editAttempted.value = true;
1327
+ const voidResult = await voidEntry({
1328
+ bookId: props.bookId,
1329
+ entryId: editingId,
1330
+ reason: t("pluginAccounting.entryForm.editVoidReason")
1331
+ });
1332
+ if (!voidResult.ok) {
1333
+ error.value = voidResult.error;
1334
+ return;
1335
+ }
1336
+ }
1337
+ const result = await addEntries({
1338
+ bookId: props.bookId,
1339
+ entries: [{
1340
+ date: date.value,
1341
+ memo: memo.value.trim() || void 0,
1342
+ lines: toApiLines(),
1343
+ ...editingId ? { replacesEntryId: editingId } : {}
1344
+ }]
1345
+ });
1346
+ if (!result.ok) {
1347
+ error.value = result.error;
1348
+ return;
1349
+ }
1350
+ successMessage.value = editingId ? t("pluginAccounting.entryForm.editSuccess") : t("pluginAccounting.entryForm.success");
1351
+ lines.value = [blankLine(), blankLine()];
1352
+ memo.value = "";
1353
+ emit("submitted");
1354
+ } catch (err) {
1355
+ error.value = errorMessage(err);
1356
+ } finally {
1357
+ submitting.value = false;
1358
+ }
1359
+ }
1360
+ watch(() => props.bookId, () => {
1361
+ lines.value = [blankLine(), blankLine()];
1362
+ memo.value = "";
1363
+ date.value = localDateString();
1364
+ error.value = null;
1365
+ successMessage.value = null;
1366
+ });
1367
+ watch(() => props.entryToEdit, (entry) => {
1368
+ error.value = null;
1369
+ successMessage.value = null;
1370
+ editAttempted.value = false;
1371
+ if (!entry) {
1372
+ lines.value = [blankLine(), blankLine()];
1373
+ memo.value = "";
1374
+ date.value = localDateString();
1375
+ return;
1376
+ }
1377
+ date.value = entry.date;
1378
+ memo.value = entry.memo ?? "";
1379
+ lines.value = entry.lines.map((line) => ({
1380
+ accountCode: line.accountCode,
1381
+ debit: typeof line.debit === "number" ? line.debit : null,
1382
+ credit: typeof line.credit === "number" ? line.credit : null,
1383
+ taxRegistrationId: line.taxRegistrationId ?? ""
1384
+ }));
1385
+ if (lines.value.length < 2) while (lines.value.length < 2) lines.value.push(blankLine());
1386
+ }, { immediate: true });
1387
+ watch(selectableAccountCodes, (codes) => {
1388
+ for (const line of lines.value) if (line.accountCode && !codes.has(line.accountCode)) line.accountCode = "";
1389
+ });
1390
+ return (_ctx, _cache) => {
1391
+ return openBlock(), createElementBlock(Fragment, null, [createElementVNode("form", {
1392
+ class: "flex flex-col gap-3",
1393
+ "data-testid": "accounting-entry-form",
1394
+ onSubmit: withModifiers(onSubmit, ["prevent"])
1395
+ }, [
1396
+ !isEditing.value ? (openBlock(), createElementBlock("h3", _hoisted_1$9, toDisplayString(unref(t)("pluginAccounting.entryForm.title")), 1)) : createCommentVNode("", true),
1397
+ createElementVNode("div", _hoisted_2$8, [createElementVNode("label", _hoisted_3$8, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.entryForm.dateLabel")) + " ", 1), withDirectives(createElementVNode("input", {
1398
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => date.value = $event),
1399
+ type: "date",
1400
+ required: "",
1401
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
1402
+ "data-testid": "accounting-entry-date"
1403
+ }, null, 512), [[vModelText, date.value]])]), createElementVNode("label", _hoisted_4$8, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.entryForm.memoLabel")) + " ", 1), withDirectives(createElementVNode("input", {
1404
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => memo.value = $event),
1405
+ type: "text",
1406
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
1407
+ "data-testid": "accounting-entry-memo"
1408
+ }, null, 512), [[vModelText, memo.value]])])]),
1409
+ createElementVNode("table", _hoisted_5$8, [createElementVNode("thead", null, [createElementVNode("tr", _hoisted_6$8, [
1410
+ createElementVNode("th", _hoisted_7$8, toDisplayString(unref(t)("pluginAccounting.entryForm.accountLabel")), 1),
1411
+ createElementVNode("th", _hoisted_8$8, toDisplayString(unref(t)("pluginAccounting.entryForm.debitLabel")), 1),
1412
+ createElementVNode("th", _hoisted_9$7, toDisplayString(unref(t)("pluginAccounting.entryForm.creditLabel")), 1),
1413
+ anyTaxLine.value ? (openBlock(), createElementBlock("th", _hoisted_10$7, toDisplayString(unref(t)("pluginAccounting.entryForm.taxRegistrationIdLabel")), 1)) : createCommentVNode("", true),
1414
+ _cache[5] || (_cache[5] = createElementVNode("th", { class: "py-1 px-2" }, null, -1))
1415
+ ])]), createElementVNode("tbody", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(lines.value, (line, idx) => {
1416
+ return openBlock(), createElementBlock("tr", {
1417
+ key: idx,
1418
+ class: "border-b border-gray-100"
1419
+ }, [
1420
+ createElementVNode("td", _hoisted_11$7, [withDirectives(createElementVNode("select", {
1421
+ "onUpdate:modelValue": ($event) => line.accountCode = $event,
1422
+ class: "h-8 px-2 w-full rounded border border-gray-300 text-sm bg-white",
1423
+ "data-testid": `accounting-entry-line-account-${idx}`
1424
+ }, [createElementVNode("option", { value: "" }, toDisplayString(DASH$1)), (openBlock(true), createElementBlock(Fragment, null, renderList(selectableAccounts.value, (account) => {
1425
+ return openBlock(), createElementBlock("option", {
1426
+ key: account.code,
1427
+ value: account.code
1428
+ }, toDisplayString(formatAccountLabel(account)), 9, _hoisted_13$7);
1429
+ }), 128))], 8, _hoisted_12$7), [[vModelSelect, line.accountCode]])]),
1430
+ createElementVNode("td", _hoisted_14$7, [withDirectives(createElementVNode("input", {
1431
+ "onUpdate:modelValue": ($event) => line.debit = $event,
1432
+ type: "number",
1433
+ step: step.value,
1434
+ min: "0",
1435
+ class: "h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white",
1436
+ "data-testid": `accounting-entry-line-debit-${idx}`,
1437
+ onInput: ($event) => onDebitInput(line)
1438
+ }, null, 40, _hoisted_15$6), [[
1439
+ vModelText,
1440
+ line.debit,
1441
+ void 0,
1442
+ { number: true }
1443
+ ]])]),
1444
+ createElementVNode("td", _hoisted_16$6, [withDirectives(createElementVNode("input", {
1445
+ "onUpdate:modelValue": ($event) => line.credit = $event,
1446
+ type: "number",
1447
+ step: step.value,
1448
+ min: "0",
1449
+ class: "h-8 px-2 w-full rounded border border-gray-300 text-sm text-right bg-white",
1450
+ "data-testid": `accounting-entry-line-credit-${idx}`,
1451
+ onInput: ($event) => onCreditInput(line)
1452
+ }, null, 40, _hoisted_17$6), [[
1453
+ vModelText,
1454
+ line.credit,
1455
+ void 0,
1456
+ { number: true }
1457
+ ]])]),
1458
+ anyTaxLine.value ? (openBlock(), createElementBlock("td", _hoisted_18$6, [isTaxLine(line) ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [withDirectives(createElementVNode("input", {
1459
+ "onUpdate:modelValue": ($event) => line.taxRegistrationId = $event,
1460
+ type: "text",
1461
+ maxlength: MAX_TAX_REGISTRATION_ID_LENGTH,
1462
+ placeholder: unref(t)("pluginAccounting.entryForm.taxRegistrationIdPlaceholder"),
1463
+ class: normalizeClass(["h-8 px-2 w-full rounded border text-sm font-mono bg-white focus:outline-none", isTaxRegistrationIdInvalid(line) ? "border-red-500 ring-1 ring-red-500" : isTaxRegistrationIdMissing(line) ? "border-amber-500 ring-1 ring-amber-500" : "border-gray-300 focus:ring-1 focus:ring-blue-500"]),
1464
+ "data-testid": `accounting-entry-line-tax-registration-id-${idx}`,
1465
+ "aria-describedby": isTaxRegistrationIdMissing(line) ? `accounting-entry-line-tax-registration-id-warning-${idx}` : void 0
1466
+ }, null, 10, _hoisted_19$6), [[vModelText, line.taxRegistrationId]]), isTaxRegistrationIdMissing(line) ? (openBlock(), createElementBlock("p", {
1467
+ key: 0,
1468
+ id: `accounting-entry-line-tax-registration-id-warning-${idx}`,
1469
+ class: "text-xs text-amber-600 mt-1",
1470
+ role: "status",
1471
+ "aria-live": "polite",
1472
+ "data-testid": `accounting-entry-line-tax-registration-id-warning-${idx}`
1473
+ }, toDisplayString(unref(t)("pluginAccounting.entryForm.taxRegistrationIdMissingWarning")), 9, _hoisted_20$6)) : createCommentVNode("", true)], 64)) : createCommentVNode("", true)])) : createCommentVNode("", true),
1474
+ createElementVNode("td", _hoisted_21$6, [lines.value.length > 2 ? (openBlock(), createElementBlock("button", {
1475
+ key: 0,
1476
+ type: "button",
1477
+ class: "text-xs text-red-500 hover:underline",
1478
+ onClick: ($event) => lines.value.splice(idx, 1)
1479
+ }, toDisplayString(unref(t)("pluginAccounting.entryForm.removeLine")), 9, _hoisted_22$5)) : createCommentVNode("", true)])
1480
+ ]);
1481
+ }), 128))])]),
1482
+ createElementVNode("div", _hoisted_23$5, [createElementVNode("div", _hoisted_24$5, [createElementVNode("button", {
1483
+ type: "button",
1484
+ 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",
1485
+ "data-testid": "accounting-entry-add-line",
1486
+ onClick: addLine
1487
+ }, [_cache[6] || (_cache[6] = createElementVNode("span", { class: "material-icons text-base" }, "add", -1)), createElementVNode("span", null, toDisplayString(unref(t)("pluginAccounting.entryForm.addLine")), 1)]), createElementVNode("button", {
1488
+ type: "button",
1489
+ 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",
1490
+ "data-testid": "accounting-entry-manage-accounts",
1491
+ onClick: _cache[2] || (_cache[2] = ($event) => showAccountsModal.value = true)
1492
+ }, [_cache[7] || (_cache[7] = createElementVNode("span", { class: "material-icons text-base" }, "tune", -1)), createElementVNode("span", null, toDisplayString(unref(t)("pluginAccounting.accounts.manageButton")), 1)])]), createElementVNode("span", {
1493
+ class: normalizeClass([balanced.value ? "text-green-600" : "text-red-500", "text-xs"]),
1494
+ "data-testid": "accounting-entry-balance"
1495
+ }, toDisplayString(balanced.value ? unref(t)("pluginAccounting.entryForm.balanced") : unref(t)("pluginAccounting.entryForm.imbalance", { amount: imbalanceText.value })), 3)]),
1496
+ error.value ? (openBlock(), createElementBlock("p", _hoisted_25$5, toDisplayString(error.value), 1)) : createCommentVNode("", true),
1497
+ successMessage.value ? (openBlock(), createElementBlock("p", _hoisted_26$4, toDisplayString(successMessage.value), 1)) : createCommentVNode("", true),
1498
+ createElementVNode("div", _hoisted_27$3, [isEditing.value ? (openBlock(), createElementBlock("p", _hoisted_28$2, toDisplayString(unref(t)("pluginAccounting.entryForm.editBanner")), 1)) : (openBlock(), createElementBlock("span", _hoisted_29$2)), createElementVNode("div", _hoisted_30$2, [createElementVNode("button", {
1499
+ type: "button",
1500
+ class: "h-8 px-3 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50",
1501
+ disabled: submitting.value,
1502
+ "data-testid": "accounting-entry-cancel-edit",
1503
+ onClick: _cache[3] || (_cache[3] = ($event) => emit("cancel"))
1504
+ }, toDisplayString(unref(t)("pluginAccounting.common.cancel")), 9, _hoisted_31$2), createElementVNode("button", {
1505
+ type: "submit",
1506
+ class: "h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50",
1507
+ disabled: !balanced.value || submitting.value || editLocked.value,
1508
+ "data-testid": "accounting-entry-submit"
1509
+ }, toDisplayString(submitButtonLabel.value), 9, _hoisted_32$2)])])
1510
+ ], 32), showAccountsModal.value ? (openBlock(), createBlock(AccountsModal_default, {
1511
+ key: 0,
1512
+ "book-id": __props.bookId,
1513
+ accounts: __props.accounts,
1514
+ onClose: _cache[4] || (_cache[4] = ($event) => showAccountsModal.value = false)
1515
+ }, null, 8, ["book-id", "accounts"])) : createCommentVNode("", true)], 64);
1516
+ };
1517
+ }
1518
+ });
1519
+ //#endregion
1520
+ //#region \0plugin-vue:export-helper
1521
+ var _plugin_vue_export_helper_default = (sfc, props) => {
1522
+ const target = sfc.__vccOpts || sfc;
1523
+ for (const [key, val] of props) target[key] = val;
1524
+ return target;
1525
+ };
1526
+ //#endregion
1527
+ //#region src/vue/components/JournalEntryForm.vue
1528
+ var JournalEntryForm_default = /*#__PURE__*/ _plugin_vue_export_helper_default(JournalEntryForm_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-22376c52"]]);
1529
+ //#endregion
1530
+ //#region src/vue/components/JournalList.vue?vue&type=script&setup=true&lang.ts
1531
+ var _hoisted_1$8 = { class: "flex flex-col h-full gap-3" };
1532
+ var _hoisted_2$7 = {
1533
+ key: 0,
1534
+ class: "border border-gray-200 rounded p-3",
1535
+ "data-testid": "accounting-journal-inline-form"
1536
+ };
1537
+ var _hoisted_3$7 = {
1538
+ key: 1,
1539
+ class: "flex items-center justify-end"
1540
+ };
1541
+ var _hoisted_4$7 = { class: "flex flex-wrap items-end gap-2" };
1542
+ var _hoisted_5$7 = { class: "text-xs text-gray-500 flex flex-col gap-1" };
1543
+ var _hoisted_6$7 = { value: "" };
1544
+ var _hoisted_7$7 = ["value"];
1545
+ var _hoisted_8$7 = { class: "flex-1 min-h-0 overflow-auto" };
1546
+ var _hoisted_9$6 = {
1547
+ key: 0,
1548
+ class: "text-xs text-gray-400"
1549
+ };
1550
+ var _hoisted_10$6 = {
1551
+ key: 1,
1552
+ class: "text-xs text-red-500"
1553
+ };
1554
+ var _hoisted_11$6 = {
1555
+ key: 2,
1556
+ class: "text-xs text-gray-400"
1557
+ };
1558
+ var _hoisted_12$6 = {
1559
+ key: 3,
1560
+ class: "w-full text-sm",
1561
+ "data-testid": "accounting-journal-table"
1562
+ };
1563
+ var _hoisted_13$6 = { class: "text-xs text-gray-500 border-b border-gray-200" };
1564
+ var _hoisted_14$6 = { class: "sticky top-0 bg-white text-left py-1 px-2" };
1565
+ var _hoisted_15$5 = { class: "sticky top-0 bg-white text-left py-1 px-2" };
1566
+ var _hoisted_16$5 = { class: "sticky top-0 bg-white text-left py-1 px-2" };
1567
+ var _hoisted_17$5 = { class: "sticky top-0 bg-white text-left py-1 px-2" };
1568
+ var _hoisted_18$5 = [
1569
+ "data-testid",
1570
+ "aria-expanded",
1571
+ "onClick",
1572
+ "onKeydown"
1573
+ ];
1574
+ var _hoisted_19$5 = { class: "py-1 px-2 whitespace-nowrap" };
1575
+ var _hoisted_20$5 = { class: "py-1 px-2 text-xs" };
1576
+ var _hoisted_21$5 = { class: "py-1 px-2" };
1577
+ var _hoisted_22$4 = { key: 0 };
1578
+ var _hoisted_23$4 = { class: "py-1 px-2" };
1579
+ var _hoisted_24$4 = { class: "font-mono text-[10px] text-gray-400" };
1580
+ var _hoisted_25$4 = { key: 0 };
1581
+ var _hoisted_26$3 = { key: 1 };
1582
+ var _hoisted_27$2 = { key: 2 };
1583
+ var _hoisted_28$1 = {
1584
+ key: 1,
1585
+ class: "flex items-center justify-between gap-2"
1586
+ };
1587
+ var _hoisted_29$1 = { class: "text-xs text-gray-400 font-mono" };
1588
+ var _hoisted_30$1 = ["data-testid", "aria-label"];
1589
+ var _hoisted_31$1 = ["data-testid"];
1590
+ var _hoisted_32$1 = {
1591
+ colspan: 4,
1592
+ class: "px-6 py-2"
1593
+ };
1594
+ var _hoisted_33 = ["data-testid"];
1595
+ var _hoisted_34 = { class: "flex items-center gap-3 mb-2" };
1596
+ var _hoisted_35 = ["data-testid", "onClick"];
1597
+ var _hoisted_36 = ["data-testid", "onClick"];
1598
+ var _hoisted_37 = ["data-testid"];
1599
+ var _hoisted_38 = { class: "w-full text-xs" };
1600
+ var _hoisted_39 = { class: "text-gray-500 border-b border-gray-200" };
1601
+ var _hoisted_40 = { class: "text-left py-1 px-2" };
1602
+ var _hoisted_41 = { class: "text-right py-1 px-2" };
1603
+ var _hoisted_42 = { class: "text-right py-1 px-2" };
1604
+ var _hoisted_43 = { class: "text-left py-1 px-2" };
1605
+ var _hoisted_44 = {
1606
+ key: 0,
1607
+ class: "text-left py-1 px-2"
1608
+ };
1609
+ var _hoisted_45 = { class: "py-1 px-2" };
1610
+ var _hoisted_46 = { class: "font-mono text-[10px] text-gray-400 mr-2" };
1611
+ var _hoisted_47 = { key: 0 };
1612
+ var _hoisted_48 = { class: "py-1 px-2 text-right font-mono" };
1613
+ var _hoisted_49 = { class: "py-1 px-2 text-right font-mono" };
1614
+ var _hoisted_50 = { class: "py-1 px-2" };
1615
+ var _hoisted_51 = {
1616
+ key: 0,
1617
+ class: "py-1 px-2 font-mono text-[10px]"
1618
+ };
1619
+ var _hoisted_52 = { class: "font-semibold border-t border-gray-300 text-gray-700" };
1620
+ var _hoisted_53 = { class: "py-1 px-2 text-gray-500" };
1621
+ var _hoisted_54 = { class: "py-1 px-2 text-right font-mono" };
1622
+ var _hoisted_55 = { class: "py-1 px-2 text-right font-mono" };
1623
+ var _hoisted_56 = ["colspan"];
1624
+ //#endregion
1625
+ //#region src/vue/components/JournalList.vue
1626
+ var JournalList_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
1627
+ __name: "JournalList",
1628
+ props: {
1629
+ bookId: {},
1630
+ accounts: {},
1631
+ currency: {},
1632
+ country: {},
1633
+ version: {},
1634
+ fiscalYearEnd: {},
1635
+ openingDate: {},
1636
+ preselectEntryId: {}
1637
+ },
1638
+ emits: ["editOpening", "preselectConsumed"],
1639
+ setup(__props, { emit: __emit }) {
1640
+ const { t } = useI18n();
1641
+ const props = __props;
1642
+ const emit = __emit;
1643
+ const showNewForm = ref(false);
1644
+ const entryBeingEdited = ref(null);
1645
+ const expandedEntryId = ref(null);
1646
+ function onOpenNewEntry() {
1647
+ entryBeingEdited.value = null;
1648
+ showNewForm.value = true;
1649
+ }
1650
+ function onEditEntry(entry) {
1651
+ showNewForm.value = false;
1652
+ entryBeingEdited.value = entry;
1653
+ }
1654
+ function closeForm() {
1655
+ showNewForm.value = false;
1656
+ entryBeingEdited.value = null;
1657
+ }
1658
+ function onFormSubmitted() {
1659
+ closeForm();
1660
+ expandedEntryId.value = null;
1661
+ refresh();
1662
+ }
1663
+ function onFormCancel() {
1664
+ closeForm();
1665
+ }
1666
+ watch(() => props.bookId, () => {
1667
+ closeForm();
1668
+ expandedEntryId.value = null;
1669
+ });
1670
+ const resolvedFiscalYearEnd = computed(() => resolveFiscalYearEnd(props.fiscalYearEnd));
1671
+ const range = ref(currentFiscalYearRange(resolvedFiscalYearEnd.value));
1672
+ const accountCode = ref("");
1673
+ const entries = ref([]);
1674
+ const serverVoidedIds = ref([]);
1675
+ const loading = ref(false);
1676
+ const error = ref(null);
1677
+ const { begin: beginRequest, isCurrent } = useLatestRequest();
1678
+ function kindLabel(kind) {
1679
+ if (kind === "opening") return t("pluginAccounting.journalList.kind.opening");
1680
+ if (kind === "void") return t("pluginAccounting.journalList.kind.void");
1681
+ if (kind === "void-marker") return t("pluginAccounting.journalList.kind.voidMarker");
1682
+ return t("pluginAccounting.journalList.kind.normal");
1683
+ }
1684
+ function formatDebit(value) {
1685
+ return `DR ${formatAmount(value, props.currency)}`;
1686
+ }
1687
+ function formatCredit(value) {
1688
+ return `CR ${formatAmount(value, props.currency)}`;
1689
+ }
1690
+ function formatAccountLabel(account) {
1691
+ return `${account.name} (${account.code})`;
1692
+ }
1693
+ function formatCreatedAt(iso) {
1694
+ const date = new Date(iso);
1695
+ if (Number.isNaN(date.getTime())) return `(${iso})`;
1696
+ const pad = (num) => String(num).padStart(2, "0");
1697
+ return `(${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())})`;
1698
+ }
1699
+ const accountNameByCode = computed(() => {
1700
+ const map = /* @__PURE__ */ new Map();
1701
+ for (const account of props.accounts) map.set(account.code, account.name);
1702
+ return map;
1703
+ });
1704
+ function accountNameFor(code) {
1705
+ return accountNameByCode.value.get(code) ?? null;
1706
+ }
1707
+ function onCloseDetail() {
1708
+ expandedEntryId.value = null;
1709
+ entryBeingEdited.value = null;
1710
+ }
1711
+ function toggleExpanded(entryId) {
1712
+ if (entryBeingEdited.value?.id === entryId) return;
1713
+ expandedEntryId.value = expandedEntryId.value === entryId ? null : entryId;
1714
+ entryBeingEdited.value = null;
1715
+ }
1716
+ function onKeyToggle(event, entryId) {
1717
+ if (event.repeat) return;
1718
+ toggleExpanded(entryId);
1719
+ }
1720
+ function entryHasTaxIds(entry) {
1721
+ return entry.lines.some((line) => Boolean(line.taxRegistrationId));
1722
+ }
1723
+ function sumLines(lines, pick) {
1724
+ return lines.reduce((acc, line) => acc + (pick(line) ?? 0), 0);
1725
+ }
1726
+ function entryDebitTotal(entry) {
1727
+ return sumLines(entry.lines, (line) => line.debit);
1728
+ }
1729
+ function entryCreditTotal(entry) {
1730
+ return sumLines(entry.lines, (line) => line.credit);
1731
+ }
1732
+ async function refresh() {
1733
+ const token = beginRequest();
1734
+ loading.value = true;
1735
+ error.value = null;
1736
+ try {
1737
+ const result = await getJournalEntries({
1738
+ bookId: props.bookId,
1739
+ from: range.value.from || void 0,
1740
+ to: range.value.to || void 0,
1741
+ accountCode: accountCode.value || void 0
1742
+ });
1743
+ if (!isCurrent(token)) return;
1744
+ if (!result.ok) {
1745
+ error.value = result.error;
1746
+ entries.value = [];
1747
+ serverVoidedIds.value = [];
1748
+ return;
1749
+ }
1750
+ entries.value = result.data.entries;
1751
+ serverVoidedIds.value = result.data.voidedEntryIds;
1752
+ } finally {
1753
+ if (isCurrent(token)) loading.value = false;
1754
+ }
1755
+ }
1756
+ const filteredEntries = computed(() => entries.value);
1757
+ const visibleEntries = computed(() => {
1758
+ const list = filteredEntries.value;
1759
+ const editing = entryBeingEdited.value;
1760
+ if (editing && !list.some((entry) => entry.id === editing.id)) return [editing, ...list];
1761
+ return list;
1762
+ });
1763
+ const voidedEntryIds = computed(() => new Set(serverVoidedIds.value));
1764
+ async function onVoid(entry) {
1765
+ const reason = window.prompt(t("pluginAccounting.journalList.voidReason"));
1766
+ if (reason === null) return;
1767
+ try {
1768
+ const result = await voidEntry({
1769
+ entryId: entry.id,
1770
+ reason: reason || void 0,
1771
+ bookId: props.bookId
1772
+ });
1773
+ if (!result.ok) error.value = result.error;
1774
+ } catch (err) {
1775
+ error.value = errorMessage(err);
1776
+ }
1777
+ }
1778
+ watch(() => [props.bookId, resolvedFiscalYearEnd.value], () => {
1779
+ range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);
1780
+ });
1781
+ watch(() => [
1782
+ props.bookId,
1783
+ props.version,
1784
+ range.value.from,
1785
+ range.value.to,
1786
+ accountCode.value
1787
+ ], refresh, { immediate: true });
1788
+ const pendingPreselectId = ref(null);
1789
+ watch(() => props.preselectEntryId, (incoming) => {
1790
+ if (incoming) pendingPreselectId.value = incoming;
1791
+ }, { immediate: true });
1792
+ watch([pendingPreselectId, entries], async ([targetId, list]) => {
1793
+ if (!targetId) return;
1794
+ if (!list.some((entry) => entry.id === targetId)) return;
1795
+ if (entryBeingEdited.value) {
1796
+ pendingPreselectId.value = null;
1797
+ emit("preselectConsumed");
1798
+ return;
1799
+ }
1800
+ expandedEntryId.value = targetId;
1801
+ await nextTick();
1802
+ (document.querySelector(`[data-testid="accounting-journal-row-${targetId}"]`) ?? document.querySelector(`[data-testid="accounting-journal-row-voided-${targetId}"]`))?.scrollIntoView({
1803
+ behavior: "smooth",
1804
+ block: "center"
1805
+ });
1806
+ pendingPreselectId.value = null;
1807
+ emit("preselectConsumed");
1808
+ });
1809
+ return (_ctx, _cache) => {
1810
+ return openBlock(), createElementBlock("div", _hoisted_1$8, [
1811
+ showNewForm.value ? (openBlock(), createElementBlock("div", _hoisted_2$7, [createVNode(JournalEntryForm_default, {
1812
+ "book-id": __props.bookId,
1813
+ accounts: __props.accounts,
1814
+ currency: __props.currency,
1815
+ country: __props.country,
1816
+ "entry-to-edit": null,
1817
+ onSubmitted: onFormSubmitted,
1818
+ onCancel: onFormCancel
1819
+ }, null, 8, [
1820
+ "book-id",
1821
+ "accounts",
1822
+ "currency",
1823
+ "country"
1824
+ ])])) : (openBlock(), createElementBlock("div", _hoisted_3$7, [createElementVNode("button", {
1825
+ type: "button",
1826
+ 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",
1827
+ "data-testid": "accounting-journal-new-entry",
1828
+ onClick: onOpenNewEntry
1829
+ }, [_cache[3] || (_cache[3] = createElementVNode("span", { class: "material-icons text-base" }, "add", -1)), createElementVNode("span", null, toDisplayString(unref(t)("pluginAccounting.tabs.newEntry")), 1)])])),
1830
+ createElementVNode("div", _hoisted_4$7, [
1831
+ createVNode(DateRangePicker_default, {
1832
+ modelValue: range.value,
1833
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => range.value = $event),
1834
+ "fiscal-year-end": resolvedFiscalYearEnd.value,
1835
+ "opening-date": __props.openingDate
1836
+ }, null, 8, [
1837
+ "modelValue",
1838
+ "fiscal-year-end",
1839
+ "opening-date"
1840
+ ]),
1841
+ createElementVNode("label", _hoisted_5$7, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.journalList.accountLabel")) + " ", 1), withDirectives(createElementVNode("select", {
1842
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => accountCode.value = $event),
1843
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
1844
+ "data-testid": "accounting-journal-account"
1845
+ }, [createElementVNode("option", _hoisted_6$7, toDisplayString(unref(t)("pluginAccounting.journalList.allAccounts")), 1), (openBlock(true), createElementBlock(Fragment, null, renderList(__props.accounts, (account) => {
1846
+ return openBlock(), createElementBlock("option", {
1847
+ key: account.code,
1848
+ value: account.code
1849
+ }, toDisplayString(formatAccountLabel(account)), 9, _hoisted_7$7);
1850
+ }), 128))], 512), [[vModelSelect, accountCode.value]])]),
1851
+ createElementVNode("button", {
1852
+ class: "h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50",
1853
+ onClick: refresh
1854
+ }, [..._cache[4] || (_cache[4] = [createElementVNode("span", { class: "material-icons text-base align-middle" }, "refresh", -1)])])
1855
+ ]),
1856
+ createElementVNode("div", _hoisted_8$7, [loading.value ? (openBlock(), createElementBlock("p", _hoisted_9$6, toDisplayString(unref(t)("pluginAccounting.common.loading")), 1)) : error.value ? (openBlock(), createElementBlock("p", _hoisted_10$6, toDisplayString(unref(t)("pluginAccounting.common.error", { error: error.value })), 1)) : visibleEntries.value.length === 0 ? (openBlock(), createElementBlock("p", _hoisted_11$6, toDisplayString(unref(t)("pluginAccounting.common.empty")), 1)) : (openBlock(), createElementBlock("table", _hoisted_12$6, [createElementVNode("thead", null, [createElementVNode("tr", _hoisted_13$6, [
1857
+ createElementVNode("th", _hoisted_14$6, toDisplayString(unref(t)("pluginAccounting.journalList.columns.date")), 1),
1858
+ createElementVNode("th", _hoisted_15$5, toDisplayString(unref(t)("pluginAccounting.journalList.columns.kind")), 1),
1859
+ createElementVNode("th", _hoisted_16$5, toDisplayString(unref(t)("pluginAccounting.journalList.columns.memo")), 1),
1860
+ createElementVNode("th", _hoisted_17$5, toDisplayString(unref(t)("pluginAccounting.journalList.columns.lines")), 1)
1861
+ ])]), createElementVNode("tbody", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(visibleEntries.value, (entry) => {
1862
+ return openBlock(), createElementBlock(Fragment, { key: entry.id }, [createElementVNode("tr", {
1863
+ class: normalizeClass([
1864
+ voidedEntryIds.value.has(entry.id) ? "text-gray-400 line-through" : "",
1865
+ expandedEntryId.value === entry.id ? "row-selected" : "",
1866
+ "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"
1867
+ ]),
1868
+ "data-testid": voidedEntryIds.value.has(entry.id) ? `accounting-journal-row-voided-${entry.id}` : `accounting-journal-row-${entry.id}`,
1869
+ tabindex: "0",
1870
+ role: "button",
1871
+ "aria-expanded": expandedEntryId.value === entry.id,
1872
+ onClick: ($event) => toggleExpanded(entry.id),
1873
+ onKeydown: [withKeys(withModifiers(($event) => onKeyToggle($event, entry.id), ["prevent", "self"]), ["enter"]), withKeys(withModifiers(($event) => onKeyToggle($event, entry.id), ["prevent", "self"]), ["space"])]
1874
+ }, [
1875
+ createElementVNode("td", _hoisted_19$5, toDisplayString(entry.date), 1),
1876
+ createElementVNode("td", _hoisted_20$5, toDisplayString(kindLabel(entry.kind)), 1),
1877
+ createElementVNode("td", _hoisted_21$5, [entry.memo ? (openBlock(), createElementBlock("span", _hoisted_22$4, toDisplayString(entry.memo), 1)) : createCommentVNode("", true)]),
1878
+ createElementVNode("td", _hoisted_23$4, [expandedEntryId.value !== entry.id ? (openBlock(true), createElementBlock(Fragment, { key: 0 }, renderList(entry.lines, (line, idx) => {
1879
+ return openBlock(), createElementBlock("div", {
1880
+ key: idx,
1881
+ class: "text-xs flex gap-2 items-baseline"
1882
+ }, [
1883
+ createElementVNode("span", _hoisted_24$4, toDisplayString(line.accountCode), 1),
1884
+ accountNameFor(line.accountCode) ? (openBlock(), createElementBlock("span", _hoisted_25$4, toDisplayString(accountNameFor(line.accountCode)), 1)) : createCommentVNode("", true),
1885
+ line.debit ? (openBlock(), createElementBlock("span", _hoisted_26$3, toDisplayString(formatDebit(line.debit)), 1)) : createCommentVNode("", true),
1886
+ line.credit ? (openBlock(), createElementBlock("span", _hoisted_27$2, toDisplayString(formatCredit(line.credit)), 1)) : createCommentVNode("", true)
1887
+ ]);
1888
+ }), 128)) : (openBlock(), createElementBlock("div", _hoisted_28$1, [createElementVNode("span", _hoisted_29$1, toDisplayString(formatCreatedAt(entry.createdAt)), 1), createElementVNode("button", {
1889
+ type: "button",
1890
+ class: "h-6 w-6 flex items-center justify-center rounded text-gray-500 hover:bg-gray-100",
1891
+ "data-testid": `accounting-journal-detail-close-${entry.id}`,
1892
+ "aria-label": unref(t)("common.close"),
1893
+ onClick: withModifiers(onCloseDetail, ["stop"])
1894
+ }, [..._cache[5] || (_cache[5] = [createElementVNode("span", { class: "material-icons text-base" }, "close", -1)])], 8, _hoisted_30$1)]))])
1895
+ ], 42, _hoisted_18$5), expandedEntryId.value === entry.id ? (openBlock(), createElementBlock("tr", {
1896
+ key: 0,
1897
+ class: "bg-gray-50 detail-selected",
1898
+ "data-testid": `accounting-journal-detail-${entry.id}`
1899
+ }, [createElementVNode("td", _hoisted_32$1, [entryBeingEdited.value?.id === entry.id ? (openBlock(), createElementBlock("div", {
1900
+ key: 0,
1901
+ "data-testid": `accounting-journal-detail-edit-${entry.id}`
1902
+ }, [createVNode(JournalEntryForm_default, {
1903
+ "book-id": __props.bookId,
1904
+ accounts: __props.accounts,
1905
+ currency: __props.currency,
1906
+ country: __props.country,
1907
+ "entry-to-edit": entryBeingEdited.value,
1908
+ onSubmitted: onFormSubmitted,
1909
+ onCancel: onFormCancel
1910
+ }, null, 8, [
1911
+ "book-id",
1912
+ "accounts",
1913
+ "currency",
1914
+ "country",
1915
+ "entry-to-edit"
1916
+ ])], 8, _hoisted_33)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("div", _hoisted_34, [entry.kind === "normal" && !voidedEntryIds.value.has(entry.id) ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("button", {
1917
+ class: "text-xs text-blue-600 hover:underline",
1918
+ "data-testid": `accounting-edit-${entry.id}`,
1919
+ onClick: ($event) => onEditEntry(entry)
1920
+ }, toDisplayString(unref(t)("pluginAccounting.journalList.edit")), 9, _hoisted_35), createElementVNode("button", {
1921
+ class: "text-xs text-red-500 hover:underline",
1922
+ "data-testid": `accounting-void-${entry.id}`,
1923
+ onClick: ($event) => onVoid(entry)
1924
+ }, toDisplayString(unref(t)("pluginAccounting.journalList.void")), 9, _hoisted_36)], 64)) : entry.kind === "opening" && !voidedEntryIds.value.has(entry.id) ? (openBlock(), createElementBlock("button", {
1925
+ key: 1,
1926
+ class: "text-xs text-blue-600 hover:underline",
1927
+ "data-testid": `accounting-edit-opening-${entry.id}`,
1928
+ onClick: _cache[2] || (_cache[2] = ($event) => emit("editOpening"))
1929
+ }, toDisplayString(unref(t)("pluginAccounting.journalList.edit")), 9, _hoisted_37)) : createCommentVNode("", true)]), createElementVNode("table", _hoisted_38, [
1930
+ createElementVNode("thead", null, [createElementVNode("tr", _hoisted_39, [
1931
+ createElementVNode("th", _hoisted_40, toDisplayString(unref(t)("pluginAccounting.entryForm.accountLabel")), 1),
1932
+ createElementVNode("th", _hoisted_41, toDisplayString(unref(t)("pluginAccounting.entryForm.debitLabel")), 1),
1933
+ createElementVNode("th", _hoisted_42, toDisplayString(unref(t)("pluginAccounting.entryForm.creditLabel")), 1),
1934
+ createElementVNode("th", _hoisted_43, toDisplayString(unref(t)("pluginAccounting.entryForm.memoLabel")), 1),
1935
+ entryHasTaxIds(entry) ? (openBlock(), createElementBlock("th", _hoisted_44, toDisplayString(unref(t)("pluginAccounting.entryForm.taxRegistrationIdLabel")), 1)) : createCommentVNode("", true)
1936
+ ])]),
1937
+ createElementVNode("tbody", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(entry.lines, (line, idx) => {
1938
+ return openBlock(), createElementBlock("tr", {
1939
+ key: idx,
1940
+ class: "border-b border-gray-100 text-gray-700"
1941
+ }, [
1942
+ createElementVNode("td", _hoisted_45, [createElementVNode("span", _hoisted_46, toDisplayString(line.accountCode), 1), accountNameFor(line.accountCode) ? (openBlock(), createElementBlock("span", _hoisted_47, toDisplayString(accountNameFor(line.accountCode)), 1)) : createCommentVNode("", true)]),
1943
+ createElementVNode("td", _hoisted_48, [line.debit ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(unref(formatAmount)(line.debit, __props.currency)), 1)], 64)) : createCommentVNode("", true)]),
1944
+ createElementVNode("td", _hoisted_49, [line.credit ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(unref(formatAmount)(line.credit, __props.currency)), 1)], 64)) : createCommentVNode("", true)]),
1945
+ createElementVNode("td", _hoisted_50, [line.memo ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(line.memo), 1)], 64)) : createCommentVNode("", true)]),
1946
+ entryHasTaxIds(entry) ? (openBlock(), createElementBlock("td", _hoisted_51, [line.taxRegistrationId ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(line.taxRegistrationId), 1)], 64)) : createCommentVNode("", true)])) : createCommentVNode("", true)
1947
+ ]);
1948
+ }), 128))]),
1949
+ createElementVNode("tfoot", null, [createElementVNode("tr", _hoisted_52, [
1950
+ createElementVNode("td", _hoisted_53, toDisplayString(unref(t)("pluginAccounting.balanceSheet.total")), 1),
1951
+ createElementVNode("td", _hoisted_54, toDisplayString(unref(formatAmount)(entryDebitTotal(entry), __props.currency)), 1),
1952
+ createElementVNode("td", _hoisted_55, toDisplayString(unref(formatAmount)(entryCreditTotal(entry), __props.currency)), 1),
1953
+ createElementVNode("td", { colspan: entryHasTaxIds(entry) ? 2 : 1 }, null, 8, _hoisted_56)
1954
+ ])])
1955
+ ])], 64))])], 8, _hoisted_31$1)) : createCommentVNode("", true)], 64);
1956
+ }), 128))])]))])
1957
+ ]);
1958
+ };
1959
+ }
1960
+ }), [["__scopeId", "data-v-404eebe9"]]);
1961
+ //#endregion
1962
+ //#region src/vue/components/OpeningBalancesForm.vue?vue&type=script&setup=true&lang.ts
1963
+ var _hoisted_1$7 = { class: "flex items-center justify-between gap-2" };
1964
+ var _hoisted_2$6 = { class: "text-base font-semibold" };
1965
+ var _hoisted_3$6 = { class: "text-xs text-gray-500" };
1966
+ var _hoisted_4$6 = {
1967
+ class: "text-xs text-blue-600",
1968
+ "data-testid": "accounting-opening-empty-hint"
1969
+ };
1970
+ var _hoisted_5$6 = {
1971
+ key: 0,
1972
+ class: "text-xs text-gray-500",
1973
+ "data-testid": "accounting-opening-existing"
1974
+ };
1975
+ var _hoisted_6$6 = {
1976
+ key: 0,
1977
+ class: "text-amber-600 ml-2"
1978
+ };
1979
+ var _hoisted_7$6 = {
1980
+ key: 1,
1981
+ class: "text-xs text-gray-400",
1982
+ "data-testid": "accounting-opening-none"
1983
+ };
1984
+ var _hoisted_8$6 = { class: "text-xs text-gray-500 flex flex-col gap-1 w-fit" };
1985
+ var _hoisted_9$5 = { class: "w-full text-sm" };
1986
+ var _hoisted_10$5 = { class: "text-xs text-gray-500 border-b border-gray-200" };
1987
+ var _hoisted_11$5 = { class: "text-left py-1 px-2" };
1988
+ var _hoisted_12$5 = { class: "text-right py-1 px-2 w-32" };
1989
+ var _hoisted_13$5 = { class: "text-right py-1 px-2 w-32" };
1990
+ var _hoisted_14$5 = { class: "py-1 px-2" };
1991
+ var _hoisted_15$4 = { class: "font-mono text-[10px] text-gray-400 mr-2" };
1992
+ var _hoisted_16$4 = { class: "ml-2 text-xs text-gray-400" };
1993
+ var _hoisted_17$4 = { class: "py-1 px-2" };
1994
+ var _hoisted_18$4 = [
1995
+ "onUpdate:modelValue",
1996
+ "step",
1997
+ "data-testid",
1998
+ "onInput"
1999
+ ];
2000
+ var _hoisted_19$4 = { class: "py-1 px-2" };
2001
+ var _hoisted_20$4 = [
2002
+ "onUpdate:modelValue",
2003
+ "step",
2004
+ "data-testid",
2005
+ "onInput"
2006
+ ];
2007
+ var _hoisted_21$4 = { class: "flex items-center justify-between" };
2008
+ var _hoisted_22$3 = { class: "text-xs text-gray-400" };
2009
+ var _hoisted_23$3 = {
2010
+ key: 2,
2011
+ class: "text-xs text-red-500",
2012
+ "data-testid": "accounting-opening-error"
2013
+ };
2014
+ var _hoisted_24$3 = {
2015
+ key: 3,
2016
+ class: "text-xs text-green-600",
2017
+ "data-testid": "accounting-opening-success"
2018
+ };
2019
+ var _hoisted_25$3 = { class: "flex justify-end" };
2020
+ var _hoisted_26$2 = ["disabled"];
2021
+ //#endregion
2022
+ //#region src/vue/components/OpeningBalancesForm.vue
2023
+ var OpeningBalancesForm_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
2024
+ __name: "OpeningBalancesForm",
2025
+ props: {
2026
+ bookId: {},
2027
+ accounts: {},
2028
+ currency: {},
2029
+ version: {}
2030
+ },
2031
+ emits: ["submitted"],
2032
+ setup(__props, { emit: __emit }) {
2033
+ const { t } = useI18n();
2034
+ const props = __props;
2035
+ const emit = __emit;
2036
+ const showAccountsModal = ref(false);
2037
+ const asOfDate = ref(localDateString());
2038
+ const rows = ref({});
2039
+ const existing = ref(null);
2040
+ const submitting = ref(false);
2041
+ const error = ref(null);
2042
+ const successMessage = ref(null);
2043
+ const { begin: beginLoad, isCurrent: isCurrentLoad } = useLatestRequest();
2044
+ const bsAccounts = computed(() => props.accounts.filter((account) => (account.type === "asset" || account.type === "liability" || account.type === "equity") && account.active !== false));
2045
+ function ensureRows() {
2046
+ for (const account of bsAccounts.value) if (!rows.value[account.code]) rows.value[account.code] = {
2047
+ debit: null,
2048
+ credit: null
2049
+ };
2050
+ }
2051
+ function onDebitInput(code) {
2052
+ const row = rows.value[code];
2053
+ if (row.debit !== null && row.debit !== 0) row.credit = null;
2054
+ }
2055
+ function onCreditInput(code) {
2056
+ const row = rows.value[code];
2057
+ if (row.credit !== null && row.credit !== 0) row.debit = null;
2058
+ }
2059
+ const imbalance = computed(() => {
2060
+ let sum = 0;
2061
+ for (const account of bsAccounts.value) {
2062
+ const row = rows.value[account.code];
2063
+ if (!row) continue;
2064
+ if (typeof row.debit === "number") sum += row.debit;
2065
+ if (typeof row.credit === "number") sum -= row.credit;
2066
+ }
2067
+ return sum;
2068
+ });
2069
+ const balanced = computed(() => Math.abs(imbalance.value) <= .005);
2070
+ const imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));
2071
+ const step = computed(() => inputStepFor(props.currency));
2072
+ function isPositiveAmount(value) {
2073
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
2074
+ }
2075
+ function toApiLines() {
2076
+ const out = [];
2077
+ for (const account of bsAccounts.value) {
2078
+ const row = rows.value[account.code];
2079
+ if (!row) continue;
2080
+ const debitOk = isPositiveAmount(row.debit);
2081
+ const creditOk = isPositiveAmount(row.credit);
2082
+ if (!debitOk && !creditOk) continue;
2083
+ const line = { accountCode: account.code };
2084
+ if (debitOk) line.debit = row.debit;
2085
+ if (creditOk) line.credit = row.credit;
2086
+ out.push(line);
2087
+ }
2088
+ return out;
2089
+ }
2090
+ function freshRows() {
2091
+ const out = {};
2092
+ for (const account of bsAccounts.value) out[account.code] = {
2093
+ debit: null,
2094
+ credit: null
2095
+ };
2096
+ return out;
2097
+ }
2098
+ async function loadExisting() {
2099
+ const token = beginLoad();
2100
+ const next = freshRows();
2101
+ const result = await getOpeningBalances(props.bookId);
2102
+ if (!isCurrentLoad(token)) return;
2103
+ if (!result.ok) {
2104
+ existing.value = null;
2105
+ rows.value = next;
2106
+ return;
2107
+ }
2108
+ existing.value = result.data.opening;
2109
+ if (result.data.opening) {
2110
+ asOfDate.value = result.data.opening.date;
2111
+ for (const line of result.data.opening.lines) next[line.accountCode] = {
2112
+ debit: line.debit ?? null,
2113
+ credit: line.credit ?? null
2114
+ };
2115
+ } else asOfDate.value = localDateString();
2116
+ rows.value = next;
2117
+ }
2118
+ async function onSubmit() {
2119
+ if (submitting.value || !balanced.value) return;
2120
+ submitting.value = true;
2121
+ error.value = null;
2122
+ successMessage.value = null;
2123
+ try {
2124
+ const result = await setOpeningBalances({
2125
+ bookId: props.bookId,
2126
+ asOfDate: asOfDate.value,
2127
+ lines: toApiLines()
2128
+ });
2129
+ if (!result.ok) {
2130
+ error.value = result.error;
2131
+ return;
2132
+ }
2133
+ successMessage.value = t("pluginAccounting.openingForm.success");
2134
+ emit("submitted");
2135
+ } catch (err) {
2136
+ error.value = errorMessage(err);
2137
+ } finally {
2138
+ submitting.value = false;
2139
+ }
2140
+ }
2141
+ watch(() => [
2142
+ props.bookId,
2143
+ props.version,
2144
+ props.accounts.length
2145
+ ], () => {
2146
+ ensureRows();
2147
+ loadExisting();
2148
+ }, { immediate: true });
2149
+ return (_ctx, _cache) => {
2150
+ return openBlock(), createElementBlock(Fragment, null, [createElementVNode("form", {
2151
+ class: "flex flex-col gap-3",
2152
+ "data-testid": "accounting-opening-form",
2153
+ onSubmit: withModifiers(onSubmit, ["prevent"])
2154
+ }, [
2155
+ createElementVNode("div", _hoisted_1$7, [createElementVNode("h3", _hoisted_2$6, toDisplayString(unref(t)("pluginAccounting.openingForm.title")), 1), createElementVNode("button", {
2156
+ type: "button",
2157
+ 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",
2158
+ "data-testid": "accounting-opening-manage-accounts",
2159
+ onClick: _cache[0] || (_cache[0] = ($event) => showAccountsModal.value = true)
2160
+ }, [_cache[3] || (_cache[3] = createElementVNode("span", { class: "material-icons text-base" }, "tune", -1)), createElementVNode("span", null, toDisplayString(unref(t)("pluginAccounting.accounts.manageButton")), 1)])]),
2161
+ createElementVNode("p", _hoisted_3$6, toDisplayString(unref(t)("pluginAccounting.openingForm.explainer")), 1),
2162
+ createElementVNode("p", _hoisted_4$6, toDisplayString(unref(t)("pluginAccounting.openingForm.emptyHint")), 1),
2163
+ existing.value ? (openBlock(), createElementBlock("div", _hoisted_5$6, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.openingForm.setBy", { date: existing.value.date })) + " ", 1), existing.value ? (openBlock(), createElementBlock("span", _hoisted_6$6, toDisplayString(unref(t)("pluginAccounting.openingForm.replaceWarning")), 1)) : createCommentVNode("", true)])) : (openBlock(), createElementBlock("p", _hoisted_7$6, toDisplayString(unref(t)("pluginAccounting.openingForm.none")), 1)),
2164
+ createElementVNode("label", _hoisted_8$6, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.openingForm.asOfLabel")) + " ", 1), withDirectives(createElementVNode("input", {
2165
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => asOfDate.value = $event),
2166
+ type: "date",
2167
+ required: "",
2168
+ class: "h-8 px-2 rounded border border-gray-300 text-sm",
2169
+ "data-testid": "accounting-opening-asof"
2170
+ }, null, 512), [[vModelText, asOfDate.value]])]),
2171
+ createElementVNode("table", _hoisted_9$5, [createElementVNode("thead", null, [createElementVNode("tr", _hoisted_10$5, [
2172
+ createElementVNode("th", _hoisted_11$5, toDisplayString(unref(t)("pluginAccounting.entryForm.accountLabel")), 1),
2173
+ createElementVNode("th", _hoisted_12$5, toDisplayString(unref(t)("pluginAccounting.entryForm.debitLabel")), 1),
2174
+ createElementVNode("th", _hoisted_13$5, toDisplayString(unref(t)("pluginAccounting.entryForm.creditLabel")), 1)
2175
+ ])]), createElementVNode("tbody", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(bsAccounts.value, (account) => {
2176
+ return openBlock(), createElementBlock("tr", {
2177
+ key: account.code,
2178
+ class: "border-b border-gray-100"
2179
+ }, [
2180
+ createElementVNode("td", _hoisted_14$5, [
2181
+ createElementVNode("span", _hoisted_15$4, toDisplayString(account.code), 1),
2182
+ createElementVNode("span", null, toDisplayString(account.name), 1),
2183
+ createElementVNode("span", _hoisted_16$4, toDisplayString(account.type), 1)
2184
+ ]),
2185
+ createElementVNode("td", _hoisted_17$4, [withDirectives(createElementVNode("input", {
2186
+ "onUpdate:modelValue": ($event) => rows.value[account.code].debit = $event,
2187
+ type: "number",
2188
+ step: step.value,
2189
+ min: "0",
2190
+ class: "h-8 px-2 w-full rounded border border-gray-300 text-sm text-right",
2191
+ "data-testid": `accounting-opening-debit-${account.code}`,
2192
+ onInput: ($event) => onDebitInput(account.code)
2193
+ }, null, 40, _hoisted_18$4), [[
2194
+ vModelText,
2195
+ rows.value[account.code].debit,
2196
+ void 0,
2197
+ { number: true }
2198
+ ]])]),
2199
+ createElementVNode("td", _hoisted_19$4, [withDirectives(createElementVNode("input", {
2200
+ "onUpdate:modelValue": ($event) => rows.value[account.code].credit = $event,
2201
+ type: "number",
2202
+ step: step.value,
2203
+ min: "0",
2204
+ class: "h-8 px-2 w-full rounded border border-gray-300 text-sm text-right",
2205
+ "data-testid": `accounting-opening-credit-${account.code}`,
2206
+ onInput: ($event) => onCreditInput(account.code)
2207
+ }, null, 40, _hoisted_20$4), [[
2208
+ vModelText,
2209
+ rows.value[account.code].credit,
2210
+ void 0,
2211
+ { number: true }
2212
+ ]])])
2213
+ ]);
2214
+ }), 128))])]),
2215
+ createElementVNode("div", _hoisted_21$4, [createElementVNode("span", _hoisted_22$3, toDisplayString(unref(t)("pluginAccounting.openingForm.explainer2")), 1), createElementVNode("span", {
2216
+ class: normalizeClass([balanced.value ? "text-green-600" : "text-red-500", "text-xs"]),
2217
+ "data-testid": "accounting-opening-balance"
2218
+ }, toDisplayString(balanced.value ? unref(t)("pluginAccounting.entryForm.balanced") : unref(t)("pluginAccounting.entryForm.imbalance", { amount: imbalanceText.value })), 3)]),
2219
+ error.value ? (openBlock(), createElementBlock("p", _hoisted_23$3, toDisplayString(error.value), 1)) : createCommentVNode("", true),
2220
+ successMessage.value ? (openBlock(), createElementBlock("p", _hoisted_24$3, toDisplayString(successMessage.value), 1)) : createCommentVNode("", true),
2221
+ createElementVNode("div", _hoisted_25$3, [createElementVNode("button", {
2222
+ type: "submit",
2223
+ class: "h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50",
2224
+ disabled: !balanced.value || submitting.value,
2225
+ "data-testid": "accounting-opening-submit"
2226
+ }, toDisplayString(submitting.value ? unref(t)("pluginAccounting.entryForm.submitting") : unref(t)("pluginAccounting.openingForm.submit")), 9, _hoisted_26$2)])
2227
+ ], 32), showAccountsModal.value ? (openBlock(), createBlock(AccountsModal_default, {
2228
+ key: 0,
2229
+ "book-id": __props.bookId,
2230
+ accounts: __props.accounts,
2231
+ onClose: _cache[2] || (_cache[2] = ($event) => showAccountsModal.value = false)
2232
+ }, null, 8, ["book-id", "accounts"])) : createCommentVNode("", true)], 64);
2233
+ };
2234
+ }
2235
+ }), [["__scopeId", "data-v-3a945c22"]]);
2236
+ //#endregion
2237
+ //#region src/vue/components/AccountsList.vue?vue&type=script&setup=true&lang.ts
2238
+ var _hoisted_1$6 = {
2239
+ class: "flex flex-col gap-3",
2240
+ "data-testid": "accounting-accounts-list"
2241
+ };
2242
+ var _hoisted_2$5 = { class: "flex flex-wrap items-center justify-end gap-2" };
2243
+ var _hoisted_3$5 = { class: "text-xs font-semibold text-gray-500 uppercase tracking-wide" };
2244
+ var _hoisted_4$5 = {
2245
+ key: 0,
2246
+ class: "text-xs text-gray-400 italic px-1"
2247
+ };
2248
+ var _hoisted_5$5 = {
2249
+ key: 1,
2250
+ class: "flex flex-col"
2251
+ };
2252
+ var _hoisted_6$5 = [
2253
+ "aria-label",
2254
+ "data-testid",
2255
+ "onClick",
2256
+ "onKeydown"
2257
+ ];
2258
+ var _hoisted_7$5 = { class: "font-mono text-xs w-16 shrink-0" };
2259
+ var _hoisted_8$5 = { class: "text-sm flex-1 min-w-0 truncate" };
2260
+ //#endregion
2261
+ //#region src/vue/components/AccountsList.vue
2262
+ var AccountsList_default = /* @__PURE__ */ defineComponent({
2263
+ __name: "AccountsList",
2264
+ props: {
2265
+ bookId: {},
2266
+ accounts: {}
2267
+ },
2268
+ emits: ["selectAccount", "changed"],
2269
+ setup(__props, { emit: __emit }) {
2270
+ const { t } = useI18n();
2271
+ const props = __props;
2272
+ const emit = __emit;
2273
+ const ACCOUNT_TYPES = [
2274
+ "asset",
2275
+ "liability",
2276
+ "equity",
2277
+ "income",
2278
+ "expense"
2279
+ ];
2280
+ const showManageModal = ref(false);
2281
+ function byCode(left, right) {
2282
+ return left.code.localeCompare(right.code);
2283
+ }
2284
+ const groups = computed(() => ACCOUNT_TYPES.map((type) => ({
2285
+ type,
2286
+ accounts: props.accounts.filter((account) => account.type === type && account.active !== false).slice().sort(byCode)
2287
+ })));
2288
+ function onSelect(account) {
2289
+ emit("selectAccount", account.code);
2290
+ }
2291
+ function onKeyActivate(event, account) {
2292
+ if (event.repeat) return;
2293
+ emit("selectAccount", account.code);
2294
+ }
2295
+ function onAccountsChanged() {
2296
+ emit("changed");
2297
+ }
2298
+ return (_ctx, _cache) => {
2299
+ return openBlock(), createElementBlock("div", _hoisted_1$6, [
2300
+ createElementVNode("div", _hoisted_2$5, [createElementVNode("button", {
2301
+ type: "button",
2302
+ 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",
2303
+ "data-testid": "accounting-accounts-manage",
2304
+ onClick: _cache[0] || (_cache[0] = ($event) => showManageModal.value = true)
2305
+ }, [_cache[2] || (_cache[2] = createElementVNode("span", { class: "material-icons text-base" }, "tune", -1)), createElementVNode("span", null, toDisplayString(unref(t)("pluginAccounting.accounts.manageButton")), 1)])]),
2306
+ (openBlock(true), createElementBlock(Fragment, null, renderList(groups.value, (group) => {
2307
+ return openBlock(), createElementBlock("section", {
2308
+ key: group.type,
2309
+ class: "flex flex-col gap-1"
2310
+ }, [createElementVNode("h4", _hoisted_3$5, toDisplayString(unref(t)(`pluginAccounting.accounts.sectionTitle.${group.type}`)), 1), group.accounts.length === 0 ? (openBlock(), createElementBlock("p", _hoisted_4$5, toDisplayString(unref(t)("pluginAccounting.accounts.listEmpty")), 1)) : (openBlock(), createElementBlock("ul", _hoisted_5$5, [(openBlock(true), createElementBlock(Fragment, null, renderList(group.accounts, (account) => {
2311
+ return openBlock(), createElementBlock("li", {
2312
+ key: account.code,
2313
+ tabindex: "0",
2314
+ role: "button",
2315
+ "aria-label": unref(t)("pluginAccounting.accounts.openLedgerAria", {
2316
+ code: account.code,
2317
+ name: account.name
2318
+ }),
2319
+ 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",
2320
+ "data-testid": `accounting-account-row-${account.code}`,
2321
+ onClick: ($event) => onSelect(account),
2322
+ onKeydown: [withKeys(withModifiers(($event) => onKeyActivate($event, account), ["prevent", "self"]), ["enter"]), withKeys(withModifiers(($event) => onKeyActivate($event, account), ["prevent", "self"]), ["space"])]
2323
+ }, [createElementVNode("span", _hoisted_7$5, toDisplayString(account.code), 1), createElementVNode("span", _hoisted_8$5, toDisplayString(account.name), 1)], 40, _hoisted_6$5);
2324
+ }), 128))]))]);
2325
+ }), 128)),
2326
+ showManageModal.value ? (openBlock(), createBlock(AccountsModal_default, {
2327
+ key: 0,
2328
+ "book-id": __props.bookId,
2329
+ accounts: __props.accounts,
2330
+ onClose: _cache[1] || (_cache[1] = ($event) => showManageModal.value = false),
2331
+ onChanged: onAccountsChanged
2332
+ }, null, 8, ["book-id", "accounts"])) : createCommentVNode("", true)
2333
+ ]);
2334
+ };
2335
+ }
2336
+ });
2337
+ //#endregion
2338
+ //#region src/vue/components/Ledger.vue?vue&type=script&setup=true&lang.ts
2339
+ var _hoisted_1$5 = {
2340
+ class: "flex flex-col gap-3",
2341
+ "data-testid": "accounting-ledger"
2342
+ };
2343
+ var _hoisted_2$4 = { class: "flex flex-wrap items-end gap-3" };
2344
+ var _hoisted_3$4 = { class: "text-xs text-gray-500 flex flex-col gap-1" };
2345
+ var _hoisted_4$4 = ["value"];
2346
+ var _hoisted_5$4 = {
2347
+ key: 0,
2348
+ class: "text-xs text-gray-400"
2349
+ };
2350
+ var _hoisted_6$4 = {
2351
+ key: 1,
2352
+ class: "text-xs text-red-500"
2353
+ };
2354
+ var _hoisted_7$4 = ["data-testid"];
2355
+ var _hoisted_8$4 = { class: "text-xs text-gray-500 border-b border-gray-200" };
2356
+ var _hoisted_9$4 = { class: "text-left py-1 px-2" };
2357
+ var _hoisted_10$4 = { class: "text-left py-1 px-2" };
2358
+ var _hoisted_11$4 = {
2359
+ key: 0,
2360
+ class: "text-left py-1 px-2 w-40"
2361
+ };
2362
+ var _hoisted_12$4 = { class: "text-right py-1 px-2 w-28" };
2363
+ var _hoisted_13$4 = { class: "text-right py-1 px-2 w-28" };
2364
+ var _hoisted_14$4 = { class: "text-right py-1 px-2 w-28" };
2365
+ var _hoisted_15$3 = { class: "py-1 px-2 whitespace-nowrap" };
2366
+ var _hoisted_16$3 = { class: "py-1 px-2" };
2367
+ var _hoisted_17$3 = { key: 0 };
2368
+ var _hoisted_18$3 = {
2369
+ key: 0,
2370
+ class: "py-1 px-2 font-mono text-xs"
2371
+ };
2372
+ var _hoisted_19$3 = { key: 0 };
2373
+ var _hoisted_20$3 = { class: "py-1 px-2 text-right" };
2374
+ var _hoisted_21$3 = { key: 0 };
2375
+ var _hoisted_22$2 = { class: "py-1 px-2 text-right" };
2376
+ var _hoisted_23$2 = { key: 0 };
2377
+ var _hoisted_24$2 = { class: "py-1 px-2 text-right font-mono" };
2378
+ var _hoisted_25$2 = { class: "font-semibold border-t border-gray-300" };
2379
+ var _hoisted_26$1 = ["colspan"];
2380
+ var _hoisted_27$1 = { class: "py-1 px-2 text-right" };
2381
+ var DASH = "—";
2382
+ //#endregion
2383
+ //#region src/vue/components/Ledger.vue
2384
+ var Ledger_default = /* @__PURE__ */ defineComponent({
2385
+ __name: "Ledger",
2386
+ props: {
2387
+ bookId: {},
2388
+ accounts: {},
2389
+ currency: {},
2390
+ version: {},
2391
+ fiscalYearEnd: {},
2392
+ openingDate: {},
2393
+ preselectAccountCode: {}
2394
+ },
2395
+ setup(__props) {
2396
+ const { t } = useI18n();
2397
+ const props = __props;
2398
+ const accountCode = ref("");
2399
+ const ledger = ref(null);
2400
+ const loading = ref(false);
2401
+ const error = ref(null);
2402
+ const { begin: beginRequest, isCurrent } = useLatestRequest();
2403
+ const resolvedFiscalYearEnd = computed(() => resolveFiscalYearEnd(props.fiscalYearEnd));
2404
+ const range = ref(currentFiscalYearRange(resolvedFiscalYearEnd.value));
2405
+ function formatAmount$3(value) {
2406
+ return formatAmount(value, props.currency);
2407
+ }
2408
+ function formatAccountLabel(account) {
2409
+ return `${account.name} (${account.code})`;
2410
+ }
2411
+ const selectableAccounts = computed(() => props.accounts.filter((account) => account.active !== false));
2412
+ const showTaxRegistrationColumn = computed(() => {
2413
+ if (!ledger.value) return false;
2414
+ return isTaxAccountCode(ledger.value.accountCode);
2415
+ });
2416
+ function periodFromRange(value) {
2417
+ if (value.from === "" && value.to === "") return void 0;
2418
+ return {
2419
+ kind: "range",
2420
+ from: value.from || "0000-01-01",
2421
+ to: value.to || "9999-12-31"
2422
+ };
2423
+ }
2424
+ async function refresh() {
2425
+ const token = beginRequest();
2426
+ if (!accountCode.value) {
2427
+ ledger.value = null;
2428
+ error.value = null;
2429
+ loading.value = false;
2430
+ return;
2431
+ }
2432
+ loading.value = true;
2433
+ error.value = null;
2434
+ try {
2435
+ const result = await getLedger(accountCode.value, periodFromRange(range.value), props.bookId);
2436
+ if (!isCurrent(token)) return;
2437
+ if (!result.ok) {
2438
+ error.value = result.error;
2439
+ ledger.value = null;
2440
+ return;
2441
+ }
2442
+ ledger.value = result.data.ledger;
2443
+ } finally {
2444
+ if (isCurrent(token)) loading.value = false;
2445
+ }
2446
+ }
2447
+ watch(() => [props.bookId, resolvedFiscalYearEnd.value], () => {
2448
+ accountCode.value = "";
2449
+ range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);
2450
+ });
2451
+ watch(() => props.preselectAccountCode, (next) => {
2452
+ if (!next) return;
2453
+ accountCode.value = next;
2454
+ range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);
2455
+ }, { immediate: true });
2456
+ watch(() => [
2457
+ props.bookId,
2458
+ props.version,
2459
+ accountCode.value,
2460
+ range.value.from,
2461
+ range.value.to
2462
+ ], refresh, { immediate: true });
2463
+ return (_ctx, _cache) => {
2464
+ return openBlock(), createElementBlock("div", _hoisted_1$5, [createElementVNode("div", _hoisted_2$4, [
2465
+ createElementVNode("label", _hoisted_3$4, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.ledger.selectAccount")) + " ", 1), withDirectives(createElementVNode("select", {
2466
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => accountCode.value = $event),
2467
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
2468
+ "data-testid": "accounting-ledger-account"
2469
+ }, [createElementVNode("option", { value: "" }, toDisplayString(DASH)), (openBlock(true), createElementBlock(Fragment, null, renderList(selectableAccounts.value, (account) => {
2470
+ return openBlock(), createElementBlock("option", {
2471
+ key: account.code,
2472
+ value: account.code
2473
+ }, toDisplayString(formatAccountLabel(account)), 9, _hoisted_4$4);
2474
+ }), 128))], 512), [[vModelSelect, accountCode.value]])]),
2475
+ createVNode(DateRangePicker_default, {
2476
+ modelValue: range.value,
2477
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => range.value = $event),
2478
+ "fiscal-year-end": resolvedFiscalYearEnd.value,
2479
+ "opening-date": __props.openingDate
2480
+ }, null, 8, [
2481
+ "modelValue",
2482
+ "fiscal-year-end",
2483
+ "opening-date"
2484
+ ]),
2485
+ createElementVNode("button", {
2486
+ class: "h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50",
2487
+ onClick: refresh
2488
+ }, [..._cache[2] || (_cache[2] = [createElementVNode("span", { class: "material-icons text-base align-middle" }, "refresh", -1)])])
2489
+ ]), loading.value ? (openBlock(), createElementBlock("p", _hoisted_5$4, toDisplayString(unref(t)("pluginAccounting.common.loading")), 1)) : error.value ? (openBlock(), createElementBlock("p", _hoisted_6$4, toDisplayString(unref(t)("pluginAccounting.common.error", { error: error.value })), 1)) : ledger.value ? (openBlock(), createElementBlock("table", {
2490
+ key: 2,
2491
+ class: "w-full text-sm",
2492
+ "data-testid": showTaxRegistrationColumn.value ? "accounting-ledger-table-with-tax-id" : "accounting-ledger-table"
2493
+ }, [
2494
+ createElementVNode("thead", null, [createElementVNode("tr", _hoisted_8$4, [
2495
+ createElementVNode("th", _hoisted_9$4, toDisplayString(unref(t)("pluginAccounting.ledger.columns.date")), 1),
2496
+ createElementVNode("th", _hoisted_10$4, toDisplayString(unref(t)("pluginAccounting.ledger.columns.memo")), 1),
2497
+ showTaxRegistrationColumn.value ? (openBlock(), createElementBlock("th", _hoisted_11$4, toDisplayString(unref(t)("pluginAccounting.ledger.columns.taxRegistrationId")), 1)) : createCommentVNode("", true),
2498
+ createElementVNode("th", _hoisted_12$4, toDisplayString(unref(t)("pluginAccounting.ledger.columns.debit")), 1),
2499
+ createElementVNode("th", _hoisted_13$4, toDisplayString(unref(t)("pluginAccounting.ledger.columns.credit")), 1),
2500
+ createElementVNode("th", _hoisted_14$4, toDisplayString(unref(t)("pluginAccounting.ledger.columns.balance")), 1)
2501
+ ])]),
2502
+ createElementVNode("tbody", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(ledger.value.rows, (row) => {
2503
+ return openBlock(), createElementBlock("tr", {
2504
+ key: `${row.entryId}-${row.date}`,
2505
+ class: normalizeClass([row.kind === "void" || row.kind === "void-marker" ? "text-gray-400 line-through" : "", "border-b border-gray-100"])
2506
+ }, [
2507
+ createElementVNode("td", _hoisted_15$3, toDisplayString(row.date), 1),
2508
+ createElementVNode("td", _hoisted_16$3, [row.memo ? (openBlock(), createElementBlock("span", _hoisted_17$3, toDisplayString(row.memo), 1)) : createCommentVNode("", true)]),
2509
+ showTaxRegistrationColumn.value ? (openBlock(), createElementBlock("td", _hoisted_18$3, [row.taxRegistrationId ? (openBlock(), createElementBlock("span", _hoisted_19$3, toDisplayString(row.taxRegistrationId), 1)) : createCommentVNode("", true)])) : createCommentVNode("", true),
2510
+ createElementVNode("td", _hoisted_20$3, [row.debit ? (openBlock(), createElementBlock("span", _hoisted_21$3, toDisplayString(formatAmount$3(row.debit)), 1)) : createCommentVNode("", true)]),
2511
+ createElementVNode("td", _hoisted_22$2, [row.credit ? (openBlock(), createElementBlock("span", _hoisted_23$2, toDisplayString(formatAmount$3(row.credit)), 1)) : createCommentVNode("", true)]),
2512
+ createElementVNode("td", _hoisted_24$2, toDisplayString(formatAmount$3(row.runningBalance)), 1)
2513
+ ], 2);
2514
+ }), 128))]),
2515
+ createElementVNode("tfoot", null, [createElementVNode("tr", _hoisted_25$2, [createElementVNode("td", {
2516
+ colspan: showTaxRegistrationColumn.value ? 5 : 4,
2517
+ class: "py-1 px-2 text-right"
2518
+ }, toDisplayString(unref(t)("pluginAccounting.ledger.closingBalance")), 9, _hoisted_26$1), createElementVNode("td", _hoisted_27$1, toDisplayString(formatAmount$3(ledger.value.closingBalance)), 1)])])
2519
+ ], 8, _hoisted_7$4)) : createCommentVNode("", true)]);
2520
+ };
2521
+ }
2522
+ });
2523
+ //#endregion
2524
+ //#region src/vue/components/BalanceSheet.vue?vue&type=script&setup=true&lang.ts
2525
+ var _hoisted_1$4 = {
2526
+ class: "flex flex-col gap-3",
2527
+ "data-testid": "accounting-balance-sheet"
2528
+ };
2529
+ var _hoisted_2$3 = { class: "flex flex-wrap items-end gap-3" };
2530
+ var _hoisted_3$3 = { class: "text-xs text-gray-500 flex flex-col gap-1" };
2531
+ var _hoisted_4$3 = ["value"];
2532
+ var _hoisted_5$3 = { value: "thisMonth" };
2533
+ var _hoisted_6$3 = { value: "lastMonth" };
2534
+ var _hoisted_7$3 = { value: "lastQuarter" };
2535
+ var _hoisted_8$3 = { value: "lastYear" };
2536
+ var _hoisted_9$3 = { class: "text-xs text-gray-500 flex flex-col gap-1" };
2537
+ var _hoisted_10$3 = {
2538
+ key: 0,
2539
+ class: "text-xs text-gray-400"
2540
+ };
2541
+ var _hoisted_11$3 = {
2542
+ key: 1,
2543
+ class: "text-xs text-red-500"
2544
+ };
2545
+ var _hoisted_12$3 = { class: "grid grid-cols-1 md:grid-cols-2 gap-4" };
2546
+ var _hoisted_13$3 = { class: "text-sm font-semibold mb-2" };
2547
+ var _hoisted_14$3 = { class: "w-full text-sm" };
2548
+ var _hoisted_15$2 = [
2549
+ "tabindex",
2550
+ "role",
2551
+ "aria-label",
2552
+ "data-testid",
2553
+ "onClick",
2554
+ "onKeydown"
2555
+ ];
2556
+ var _hoisted_16$2 = { class: "py-1 px-1" };
2557
+ var _hoisted_17$2 = {
2558
+ key: 0,
2559
+ class: "font-mono text-[10px] text-gray-400 mr-2"
2560
+ };
2561
+ var _hoisted_18$2 = { class: "py-1 px-1 text-right font-mono" };
2562
+ var _hoisted_19$2 = { class: "font-semibold border-t border-gray-300" };
2563
+ var _hoisted_20$2 = { class: "py-1 px-1" };
2564
+ var _hoisted_21$2 = { class: "py-1 px-1 text-right" };
2565
+ var CURRENT_EARNINGS_ACCOUNT_CODE = "_currentEarnings";
2566
+ //#endregion
2567
+ //#region src/vue/components/BalanceSheet.vue
2568
+ var BalanceSheet_default = /* @__PURE__ */ defineComponent({
2569
+ __name: "BalanceSheet",
2570
+ props: {
2571
+ bookId: {},
2572
+ currency: {},
2573
+ version: {}
2574
+ },
2575
+ emits: ["selectAccount"],
2576
+ setup(__props, { emit: __emit }) {
2577
+ const { t } = useI18n();
2578
+ const props = __props;
2579
+ const emit = __emit;
2580
+ const period = ref(localMonthString());
2581
+ const balanceSheet = ref(null);
2582
+ const loading = ref(false);
2583
+ const error = ref(null);
2584
+ const { begin: beginRequest, isCurrent } = useLatestRequest();
2585
+ function formatAmount$2(value) {
2586
+ return formatAmount(value, props.currency);
2587
+ }
2588
+ function sectionLabel(type) {
2589
+ if (type === "asset") return t("pluginAccounting.balanceSheet.sections.asset");
2590
+ if (type === "liability") return t("pluginAccounting.balanceSheet.sections.liability");
2591
+ if (type === "equity") return t("pluginAccounting.balanceSheet.sections.equity");
2592
+ return type;
2593
+ }
2594
+ function isEarningsRow(row) {
2595
+ return row.accountCode === CURRENT_EARNINGS_ACCOUNT_CODE;
2596
+ }
2597
+ function rowName(row) {
2598
+ return isEarningsRow(row) ? t("pluginAccounting.balanceSheet.currentEarnings") : row.accountName;
2599
+ }
2600
+ function onRowClick(row) {
2601
+ if (isEarningsRow(row)) return;
2602
+ emit("selectAccount", row.accountCode);
2603
+ }
2604
+ function onKeyActivate(event, row) {
2605
+ if (event.repeat) return;
2606
+ if (isEarningsRow(row)) return;
2607
+ emit("selectAccount", row.accountCode);
2608
+ }
2609
+ const selectedShortcut = computed(() => {
2610
+ const { value } = period;
2611
+ const now = /* @__PURE__ */ new Date();
2612
+ if (value === localMonthString(now)) return "thisMonth";
2613
+ if (value === previousMonthString(now)) return "lastMonth";
2614
+ if (value === lastMonthOfPreviousQuarterString(now)) return "lastQuarter";
2615
+ if (value === decemberOfPreviousYearString(now)) return "lastYear";
2616
+ return "";
2617
+ });
2618
+ function onShortcutChange(raw) {
2619
+ const now = /* @__PURE__ */ new Date();
2620
+ if (raw === "thisMonth") period.value = localMonthString(now);
2621
+ else if (raw === "lastMonth") period.value = previousMonthString(now);
2622
+ else if (raw === "lastQuarter") period.value = lastMonthOfPreviousQuarterString(now);
2623
+ else if (raw === "lastYear") period.value = decemberOfPreviousYearString(now);
2624
+ }
2625
+ async function refresh() {
2626
+ const token = beginRequest();
2627
+ loading.value = true;
2628
+ error.value = null;
2629
+ try {
2630
+ const result = await getBalanceSheet({
2631
+ kind: "month",
2632
+ period: period.value
2633
+ }, props.bookId);
2634
+ if (!isCurrent(token)) return;
2635
+ if (!result.ok) {
2636
+ error.value = result.error;
2637
+ balanceSheet.value = null;
2638
+ return;
2639
+ }
2640
+ balanceSheet.value = result.data.balanceSheet;
2641
+ } finally {
2642
+ if (isCurrent(token)) loading.value = false;
2643
+ }
2644
+ }
2645
+ watch(() => [
2646
+ props.bookId,
2647
+ props.version,
2648
+ period.value
2649
+ ], refresh, { immediate: true });
2650
+ return (_ctx, _cache) => {
2651
+ return openBlock(), createElementBlock("div", _hoisted_1$4, [createElementVNode("div", _hoisted_2$3, [
2652
+ createElementVNode("label", _hoisted_3$3, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.balanceSheet.shortcutLabel")) + " ", 1), createElementVNode("select", {
2653
+ value: selectedShortcut.value,
2654
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
2655
+ "data-testid": "accounting-bs-shortcut",
2656
+ onChange: _cache[0] || (_cache[0] = ($event) => onShortcutChange($event.target.value))
2657
+ }, [
2658
+ _cache[2] || (_cache[2] = createElementVNode("option", {
2659
+ value: "",
2660
+ hidden: ""
2661
+ }, null, -1)),
2662
+ createElementVNode("option", _hoisted_5$3, toDisplayString(unref(t)("pluginAccounting.balanceSheet.thisMonth")), 1),
2663
+ createElementVNode("option", _hoisted_6$3, toDisplayString(unref(t)("pluginAccounting.balanceSheet.lastMonth")), 1),
2664
+ createElementVNode("option", _hoisted_7$3, toDisplayString(unref(t)("pluginAccounting.balanceSheet.lastQuarter")), 1),
2665
+ createElementVNode("option", _hoisted_8$3, toDisplayString(unref(t)("pluginAccounting.balanceSheet.lastYear")), 1)
2666
+ ], 40, _hoisted_4$3)]),
2667
+ createElementVNode("label", _hoisted_9$3, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.balanceSheet.asOfLabel")) + " ", 1), withDirectives(createElementVNode("input", {
2668
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => period.value = $event),
2669
+ type: "month",
2670
+ class: "h-8 px-2 rounded border border-gray-300 text-sm",
2671
+ "data-testid": "accounting-bs-period"
2672
+ }, null, 512), [[vModelText, period.value]])]),
2673
+ createElementVNode("button", {
2674
+ class: "h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50",
2675
+ onClick: refresh
2676
+ }, [..._cache[3] || (_cache[3] = [createElementVNode("span", { class: "material-icons text-base align-middle" }, "refresh", -1)])])
2677
+ ]), loading.value ? (openBlock(), createElementBlock("p", _hoisted_10$3, toDisplayString(unref(t)("pluginAccounting.common.loading")), 1)) : error.value ? (openBlock(), createElementBlock("p", _hoisted_11$3, toDisplayString(unref(t)("pluginAccounting.common.error", { error: error.value })), 1)) : balanceSheet.value ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [createElementVNode("div", _hoisted_12$3, [(openBlock(true), createElementBlock(Fragment, null, renderList(balanceSheet.value.sections, (section) => {
2678
+ return openBlock(), createElementBlock("section", {
2679
+ key: section.type,
2680
+ class: "border border-gray-200 rounded p-3"
2681
+ }, [createElementVNode("h4", _hoisted_13$3, toDisplayString(sectionLabel(section.type)), 1), createElementVNode("table", _hoisted_14$3, [createElementVNode("tbody", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(section.rows, (row) => {
2682
+ return openBlock(), createElementBlock("tr", {
2683
+ key: row.accountCode,
2684
+ class: normalizeClass(["border-b border-gray-100", isEarningsRow(row) ? "italic text-gray-600" : "hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"]),
2685
+ tabindex: isEarningsRow(row) ? -1 : 0,
2686
+ role: isEarningsRow(row) ? void 0 : "button",
2687
+ "aria-label": isEarningsRow(row) ? void 0 : unref(t)("pluginAccounting.accounts.openLedgerAria", {
2688
+ code: row.accountCode,
2689
+ name: row.accountName
2690
+ }),
2691
+ "data-testid": isEarningsRow(row) ? void 0 : `accounting-bs-row-${row.accountCode}`,
2692
+ onClick: ($event) => onRowClick(row),
2693
+ onKeydown: [withKeys(withModifiers(($event) => onKeyActivate($event, row), ["prevent", "self"]), ["enter"]), withKeys(withModifiers(($event) => onKeyActivate($event, row), ["prevent", "self"]), ["space"])]
2694
+ }, [createElementVNode("td", _hoisted_16$2, [!isEarningsRow(row) ? (openBlock(), createElementBlock("span", _hoisted_17$2, toDisplayString(row.accountCode), 1)) : createCommentVNode("", true), createTextVNode(toDisplayString(rowName(row)), 1)]), createElementVNode("td", _hoisted_18$2, toDisplayString(formatAmount$2(row.balance)), 1)], 42, _hoisted_15$2);
2695
+ }), 128))]), createElementVNode("tfoot", null, [createElementVNode("tr", _hoisted_19$2, [createElementVNode("td", _hoisted_20$2, toDisplayString(unref(t)("pluginAccounting.balanceSheet.total")), 1), createElementVNode("td", _hoisted_21$2, toDisplayString(formatAmount$2(section.total)), 1)])])])]);
2696
+ }), 128))]), createElementVNode("p", {
2697
+ class: normalizeClass([Math.abs(balanceSheet.value.imbalance) <= .01 ? "text-green-600" : "text-red-500", "text-xs"]),
2698
+ "data-testid": "accounting-bs-imbalance"
2699
+ }, toDisplayString(unref(t)("pluginAccounting.balanceSheet.imbalance", { amount: formatAmount$2(balanceSheet.value.imbalance) })), 3)], 64)) : createCommentVNode("", true)]);
2700
+ };
2701
+ }
2702
+ });
2703
+ //#endregion
2704
+ //#region src/vue/components/ProfitLoss.vue?vue&type=script&setup=true&lang.ts
2705
+ var _hoisted_1$3 = {
2706
+ class: "flex flex-col gap-3",
2707
+ "data-testid": "accounting-profit-loss"
2708
+ };
2709
+ var _hoisted_2$2 = { class: "flex flex-wrap items-end gap-3" };
2710
+ var _hoisted_3$2 = {
2711
+ key: 0,
2712
+ class: "text-xs text-gray-400"
2713
+ };
2714
+ var _hoisted_4$2 = {
2715
+ key: 1,
2716
+ class: "text-xs text-red-500"
2717
+ };
2718
+ var _hoisted_5$2 = { class: "border border-gray-200 rounded p-3" };
2719
+ var _hoisted_6$2 = { class: "text-sm font-semibold mb-2" };
2720
+ var _hoisted_7$2 = { class: "w-full text-sm" };
2721
+ var _hoisted_8$2 = [
2722
+ "aria-label",
2723
+ "data-testid",
2724
+ "onClick",
2725
+ "onKeydown"
2726
+ ];
2727
+ var _hoisted_9$2 = { class: "py-1 px-1" };
2728
+ var _hoisted_10$2 = { class: "font-mono text-[10px] text-gray-400 mr-2" };
2729
+ var _hoisted_11$2 = { class: "py-1 px-1 text-right font-mono" };
2730
+ var _hoisted_12$2 = { class: "font-semibold border-t border-gray-300" };
2731
+ var _hoisted_13$2 = { class: "py-1 px-1" };
2732
+ var _hoisted_14$2 = { class: "py-1 px-1 text-right" };
2733
+ var _hoisted_15$1 = { class: "border border-gray-200 rounded p-3" };
2734
+ var _hoisted_16$1 = { class: "text-sm font-semibold mb-2" };
2735
+ var _hoisted_17$1 = { class: "w-full text-sm" };
2736
+ var _hoisted_18$1 = [
2737
+ "aria-label",
2738
+ "data-testid",
2739
+ "onClick",
2740
+ "onKeydown"
2741
+ ];
2742
+ var _hoisted_19$1 = { class: "py-1 px-1" };
2743
+ var _hoisted_20$1 = { class: "font-mono text-[10px] text-gray-400 mr-2" };
2744
+ var _hoisted_21$1 = { class: "py-1 px-1 text-right font-mono" };
2745
+ var _hoisted_22$1 = { class: "font-semibold border-t border-gray-300" };
2746
+ var _hoisted_23$1 = { class: "py-1 px-1" };
2747
+ var _hoisted_24$1 = { class: "py-1 px-1 text-right" };
2748
+ var _hoisted_25$1 = {
2749
+ class: "flex justify-end items-center gap-2 text-sm font-semibold",
2750
+ "data-testid": "accounting-pl-net"
2751
+ };
2752
+ //#endregion
2753
+ //#region src/vue/components/ProfitLoss.vue
2754
+ var ProfitLoss_default = /* @__PURE__ */ defineComponent({
2755
+ __name: "ProfitLoss",
2756
+ props: {
2757
+ bookId: {},
2758
+ currency: {},
2759
+ version: {},
2760
+ fiscalYearEnd: {},
2761
+ openingDate: {}
2762
+ },
2763
+ emits: ["selectAccount"],
2764
+ setup(__props, { emit: __emit }) {
2765
+ const { t } = useI18n();
2766
+ const props = __props;
2767
+ const emit = __emit;
2768
+ const resolvedFiscalYearEnd = computed(() => resolveFiscalYearEnd(props.fiscalYearEnd));
2769
+ function onRowClick(code) {
2770
+ emit("selectAccount", code);
2771
+ }
2772
+ function onKeyActivate(event, code) {
2773
+ if (event.repeat) return;
2774
+ emit("selectAccount", code);
2775
+ }
2776
+ const range = ref(currentFiscalYearRange(resolvedFiscalYearEnd.value));
2777
+ const profitLoss = ref(null);
2778
+ const loading = ref(false);
2779
+ const error = ref(null);
2780
+ const { begin: beginRequest, isCurrent } = useLatestRequest();
2781
+ function formatAmount$1(value) {
2782
+ return formatAmount(value, props.currency);
2783
+ }
2784
+ async function refresh() {
2785
+ const token = beginRequest();
2786
+ loading.value = true;
2787
+ error.value = null;
2788
+ try {
2789
+ const result = await getProfitLoss({
2790
+ kind: "range",
2791
+ from: range.value.from || "0000-01-01",
2792
+ to: range.value.to || "9999-12-31"
2793
+ }, props.bookId);
2794
+ if (!isCurrent(token)) return;
2795
+ if (!result.ok) {
2796
+ error.value = result.error;
2797
+ profitLoss.value = null;
2798
+ return;
2799
+ }
2800
+ profitLoss.value = result.data.profitLoss;
2801
+ } finally {
2802
+ if (isCurrent(token)) loading.value = false;
2803
+ }
2804
+ }
2805
+ watch(() => [props.bookId, resolvedFiscalYearEnd.value], () => {
2806
+ range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);
2807
+ });
2808
+ watch(() => [
2809
+ props.bookId,
2810
+ props.version,
2811
+ range.value.from,
2812
+ range.value.to
2813
+ ], refresh, { immediate: true });
2814
+ return (_ctx, _cache) => {
2815
+ return openBlock(), createElementBlock("div", _hoisted_1$3, [createElementVNode("div", _hoisted_2$2, [createVNode(DateRangePicker_default, {
2816
+ modelValue: range.value,
2817
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => range.value = $event),
2818
+ "fiscal-year-end": resolvedFiscalYearEnd.value,
2819
+ "opening-date": __props.openingDate
2820
+ }, null, 8, [
2821
+ "modelValue",
2822
+ "fiscal-year-end",
2823
+ "opening-date"
2824
+ ]), createElementVNode("button", {
2825
+ class: "h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50",
2826
+ onClick: refresh
2827
+ }, [..._cache[1] || (_cache[1] = [createElementVNode("span", { class: "material-icons text-base align-middle" }, "refresh", -1)])])]), loading.value ? (openBlock(), createElementBlock("p", _hoisted_3$2, toDisplayString(unref(t)("pluginAccounting.common.loading")), 1)) : error.value ? (openBlock(), createElementBlock("p", _hoisted_4$2, toDisplayString(unref(t)("pluginAccounting.common.error", { error: error.value })), 1)) : profitLoss.value ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
2828
+ createElementVNode("section", _hoisted_5$2, [createElementVNode("h4", _hoisted_6$2, toDisplayString(unref(t)("pluginAccounting.profitLoss.income")), 1), createElementVNode("table", _hoisted_7$2, [createElementVNode("tbody", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(profitLoss.value.income.rows, (row) => {
2829
+ return openBlock(), createElementBlock("tr", {
2830
+ key: row.accountCode,
2831
+ 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",
2832
+ tabindex: "0",
2833
+ role: "button",
2834
+ "aria-label": unref(t)("pluginAccounting.accounts.openLedgerAria", {
2835
+ code: row.accountCode,
2836
+ name: row.accountName
2837
+ }),
2838
+ "data-testid": `accounting-pl-row-${row.accountCode}`,
2839
+ onClick: ($event) => onRowClick(row.accountCode),
2840
+ onKeydown: [withKeys(withModifiers(($event) => onKeyActivate($event, row.accountCode), ["prevent", "self"]), ["enter"]), withKeys(withModifiers(($event) => onKeyActivate($event, row.accountCode), ["prevent", "self"]), ["space"])]
2841
+ }, [createElementVNode("td", _hoisted_9$2, [createElementVNode("span", _hoisted_10$2, toDisplayString(row.accountCode), 1), createTextVNode(toDisplayString(row.accountName), 1)]), createElementVNode("td", _hoisted_11$2, toDisplayString(formatAmount$1(row.amount)), 1)], 40, _hoisted_8$2);
2842
+ }), 128))]), createElementVNode("tfoot", null, [createElementVNode("tr", _hoisted_12$2, [createElementVNode("td", _hoisted_13$2, toDisplayString(unref(t)("pluginAccounting.balanceSheet.total")), 1), createElementVNode("td", _hoisted_14$2, toDisplayString(formatAmount$1(profitLoss.value.income.total)), 1)])])])]),
2843
+ createElementVNode("section", _hoisted_15$1, [createElementVNode("h4", _hoisted_16$1, toDisplayString(unref(t)("pluginAccounting.profitLoss.expense")), 1), createElementVNode("table", _hoisted_17$1, [createElementVNode("tbody", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(profitLoss.value.expense.rows, (row) => {
2844
+ return openBlock(), createElementBlock("tr", {
2845
+ key: row.accountCode,
2846
+ 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",
2847
+ tabindex: "0",
2848
+ role: "button",
2849
+ "aria-label": unref(t)("pluginAccounting.accounts.openLedgerAria", {
2850
+ code: row.accountCode,
2851
+ name: row.accountName
2852
+ }),
2853
+ "data-testid": `accounting-pl-row-${row.accountCode}`,
2854
+ onClick: ($event) => onRowClick(row.accountCode),
2855
+ onKeydown: [withKeys(withModifiers(($event) => onKeyActivate($event, row.accountCode), ["prevent", "self"]), ["enter"]), withKeys(withModifiers(($event) => onKeyActivate($event, row.accountCode), ["prevent", "self"]), ["space"])]
2856
+ }, [createElementVNode("td", _hoisted_19$1, [createElementVNode("span", _hoisted_20$1, toDisplayString(row.accountCode), 1), createTextVNode(toDisplayString(row.accountName), 1)]), createElementVNode("td", _hoisted_21$1, toDisplayString(formatAmount$1(row.amount)), 1)], 40, _hoisted_18$1);
2857
+ }), 128))]), createElementVNode("tfoot", null, [createElementVNode("tr", _hoisted_22$1, [createElementVNode("td", _hoisted_23$1, toDisplayString(unref(t)("pluginAccounting.balanceSheet.total")), 1), createElementVNode("td", _hoisted_24$1, toDisplayString(formatAmount$1(profitLoss.value.expense.total)), 1)])])])]),
2858
+ createElementVNode("div", _hoisted_25$1, [createElementVNode("span", null, toDisplayString(unref(t)("pluginAccounting.profitLoss.netIncome")), 1), createElementVNode("span", { class: normalizeClass(profitLoss.value.netIncome >= 0 ? "text-green-600" : "text-red-500") }, toDisplayString(formatAmount$1(profitLoss.value.netIncome)), 3)])
2859
+ ], 64)) : createCommentVNode("", true)]);
2860
+ };
2861
+ }
2862
+ });
2863
+ //#endregion
2864
+ //#region src/vue/components/BookSettings.vue?vue&type=script&setup=true&lang.ts
2865
+ var _hoisted_1$2 = {
2866
+ class: "flex flex-col gap-4",
2867
+ "data-testid": "accounting-settings"
2868
+ };
2869
+ var _hoisted_2$1 = { class: "border border-gray-200 rounded p-3 flex flex-col gap-2" };
2870
+ var _hoisted_3$1 = { class: "text-sm font-semibold" };
2871
+ var _hoisted_4$1 = { class: "text-xs text-gray-500" };
2872
+ var _hoisted_5$1 = { class: "text-sm flex flex-col gap-1" };
2873
+ var _hoisted_6$1 = ["disabled"];
2874
+ var _hoisted_7$1 = { class: "text-xs text-gray-700 grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1" };
2875
+ var _hoisted_8$1 = { class: "text-gray-500" };
2876
+ var _hoisted_9$1 = { class: "text-sm flex flex-col gap-1 mt-1" };
2877
+ var _hoisted_10$1 = ["disabled"];
2878
+ var _hoisted_11$1 = { value: "" };
2879
+ var _hoisted_12$1 = ["value"];
2880
+ var _hoisted_13$1 = { class: "text-sm flex flex-col gap-1 mt-1" };
2881
+ var _hoisted_14$1 = ["disabled"];
2882
+ var _hoisted_15 = ["value"];
2883
+ var _hoisted_16 = { class: "text-xs text-gray-500" };
2884
+ var _hoisted_17 = {
2885
+ key: 0,
2886
+ class: "text-xs text-green-600",
2887
+ "data-testid": "accounting-settings-update-ok"
2888
+ };
2889
+ var _hoisted_18 = {
2890
+ key: 1,
2891
+ class: "text-xs text-red-500",
2892
+ "data-testid": "accounting-settings-update-error"
2893
+ };
2894
+ var _hoisted_19 = ["disabled"];
2895
+ var _hoisted_20 = { class: "border border-gray-200 rounded p-3 flex flex-col gap-2" };
2896
+ var _hoisted_21 = { class: "text-sm font-semibold" };
2897
+ var _hoisted_22 = { class: "text-xs text-gray-500" };
2898
+ var _hoisted_23 = {
2899
+ key: 0,
2900
+ class: "text-xs text-green-600",
2901
+ "data-testid": "accounting-settings-rebuild-ok"
2902
+ };
2903
+ var _hoisted_24 = {
2904
+ key: 1,
2905
+ class: "text-xs text-red-500",
2906
+ "data-testid": "accounting-settings-rebuild-error"
2907
+ };
2908
+ var _hoisted_25 = ["disabled"];
2909
+ var _hoisted_26 = { key: 0 };
2910
+ var _hoisted_27 = {
2911
+ key: 1,
2912
+ class: "border border-red-300 rounded p-3 flex flex-col gap-2"
2913
+ };
2914
+ var _hoisted_28 = { class: "text-sm font-semibold text-red-700" };
2915
+ var _hoisted_29 = { class: "text-xs text-gray-500" };
2916
+ var _hoisted_30 = {
2917
+ key: 0,
2918
+ class: "text-xs text-red-500",
2919
+ "data-testid": "accounting-settings-delete-error"
2920
+ };
2921
+ var _hoisted_31 = { class: "text-xs text-gray-500 flex flex-col gap-1" };
2922
+ var _hoisted_32 = ["disabled"];
2923
+ //#endregion
2924
+ //#region src/vue/components/BookSettings.vue
2925
+ var BookSettings_default = /* @__PURE__ */ defineComponent({
2926
+ __name: "BookSettings",
2927
+ props: {
2928
+ bookId: {},
2929
+ bookName: {},
2930
+ currency: {},
2931
+ country: {},
2932
+ fiscalYearEnd: {}
2933
+ },
2934
+ emits: ["deleted", "books-changed"],
2935
+ setup(__props, { emit: __emit }) {
2936
+ const { t, locale } = useI18n();
2937
+ const props = __props;
2938
+ const emit = __emit;
2939
+ const rebuilding = ref(false);
2940
+ const rebuildOk = ref(null);
2941
+ const rebuildError = ref(null);
2942
+ const deleting = ref(false);
2943
+ const deleteError = ref(null);
2944
+ const confirmName = ref("");
2945
+ const updating = ref(false);
2946
+ const updateOk = ref(null);
2947
+ const updateError = ref(null);
2948
+ const showAdvanced = ref(false);
2949
+ const selectedName = ref(props.bookName);
2950
+ const selectedCountry = ref(props.country ?? "");
2951
+ const selectedFiscalYearEnd = ref(props.fiscalYearEnd ?? "Q4");
2952
+ const countryOptions = computed(() => SUPPORTED_COUNTRY_CODES.map((code) => ({
2953
+ code,
2954
+ label: `${code} — ${localizedCountryName(code, locale.value)}`
2955
+ })));
2956
+ const fiscalYearEndOptions = computed(() => FISCAL_YEAR_ENDS.map((value) => ({
2957
+ value,
2958
+ label: t(`pluginAccounting.bookSwitcher.fiscalYearEnd${value}`)
2959
+ })));
2960
+ const hasPendingChanges = computed(() => {
2961
+ const nameChanged = selectedName.value.trim() !== props.bookName;
2962
+ const nameValid = selectedName.value.trim().length > 0;
2963
+ const countryChanged = selectedCountry.value !== (props.country ?? "");
2964
+ const fiscalChanged = selectedFiscalYearEnd.value !== resolveFiscalYearEnd(props.fiscalYearEnd);
2965
+ return nameValid && (nameChanged || countryChanged || fiscalChanged);
2966
+ });
2967
+ async function onRebuild() {
2968
+ rebuilding.value = true;
2969
+ rebuildOk.value = null;
2970
+ rebuildError.value = null;
2971
+ try {
2972
+ const result = await rebuildSnapshots(props.bookId);
2973
+ if (!result.ok) {
2974
+ rebuildError.value = result.error;
2975
+ return;
2976
+ }
2977
+ rebuildOk.value = t("pluginAccounting.settings.rebuildOk", { count: result.data.rebuilt.length });
2978
+ } finally {
2979
+ rebuilding.value = false;
2980
+ }
2981
+ }
2982
+ async function onSaveBookInfo() {
2983
+ if (updating.value) return;
2984
+ updating.value = true;
2985
+ updateOk.value = null;
2986
+ updateError.value = null;
2987
+ try {
2988
+ const rawCountry = selectedCountry.value;
2989
+ const country = rawCountry === "" || isSupportedCountryCode(rawCountry) ? rawCountry : "";
2990
+ const result = await updateBook({
2991
+ bookId: props.bookId,
2992
+ name: selectedName.value.trim(),
2993
+ country,
2994
+ fiscalYearEnd: selectedFiscalYearEnd.value
2995
+ });
2996
+ if (!result.ok) {
2997
+ updateError.value = result.error;
2998
+ return;
2999
+ }
3000
+ updateOk.value = t("pluginAccounting.settings.updateOk");
3001
+ emit("books-changed");
3002
+ } finally {
3003
+ updating.value = false;
3004
+ }
3005
+ }
3006
+ async function onDelete() {
3007
+ if (deleting.value) return;
3008
+ deleting.value = true;
3009
+ deleteError.value = null;
3010
+ try {
3011
+ const result = await deleteBook(props.bookId);
3012
+ if (!result.ok) {
3013
+ deleteError.value = result.error;
3014
+ return;
3015
+ }
3016
+ emit("deleted", props.bookName);
3017
+ emit("books-changed");
3018
+ } finally {
3019
+ deleting.value = false;
3020
+ }
3021
+ }
3022
+ watch(() => props.bookId, () => {
3023
+ rebuildOk.value = null;
3024
+ rebuildError.value = null;
3025
+ deleteError.value = null;
3026
+ confirmName.value = "";
3027
+ updateOk.value = null;
3028
+ updateError.value = null;
3029
+ selectedName.value = props.bookName;
3030
+ selectedCountry.value = props.country ?? "";
3031
+ selectedFiscalYearEnd.value = props.fiscalYearEnd ?? "Q4";
3032
+ showAdvanced.value = false;
3033
+ });
3034
+ watch(() => props.bookName, (next) => {
3035
+ selectedName.value = next;
3036
+ });
3037
+ watch(() => props.country, (next) => {
3038
+ selectedCountry.value = next ?? "";
3039
+ });
3040
+ watch(() => props.fiscalYearEnd, (next) => {
3041
+ selectedFiscalYearEnd.value = next ?? "Q4";
3042
+ });
3043
+ return (_ctx, _cache) => {
3044
+ return openBlock(), createElementBlock("div", _hoisted_1$2, [
3045
+ createElementVNode("section", _hoisted_2$1, [
3046
+ createElementVNode("h4", _hoisted_3$1, toDisplayString(unref(t)("pluginAccounting.settings.bookInfo")), 1),
3047
+ createElementVNode("p", _hoisted_4$1, toDisplayString(unref(t)("pluginAccounting.settings.bookInfoExplain")), 1),
3048
+ createElementVNode("label", _hoisted_5$1, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.bookSwitcher.nameLabel")) + " ", 1), withDirectives(createElementVNode("input", {
3049
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => selectedName.value = $event),
3050
+ type: "text",
3051
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
3052
+ "data-testid": "accounting-settings-name",
3053
+ disabled: updating.value,
3054
+ maxlength: "200"
3055
+ }, null, 8, _hoisted_6$1), [[vModelText, selectedName.value]])]),
3056
+ createElementVNode("dl", _hoisted_7$1, [createElementVNode("dt", _hoisted_8$1, toDisplayString(unref(t)("pluginAccounting.bookSwitcher.currencyLabel")), 1), createElementVNode("dd", null, toDisplayString(__props.currency), 1)]),
3057
+ createElementVNode("label", _hoisted_9$1, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.bookSwitcher.countryLabel")) + " ", 1), withDirectives(createElementVNode("select", {
3058
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => selectedCountry.value = $event),
3059
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
3060
+ "data-testid": "accounting-settings-country",
3061
+ disabled: updating.value
3062
+ }, [createElementVNode("option", _hoisted_11$1, toDisplayString(unref(t)("pluginAccounting.settings.countryUnset")), 1), (openBlock(true), createElementBlock(Fragment, null, renderList(countryOptions.value, (opt) => {
3063
+ return openBlock(), createElementBlock("option", {
3064
+ key: opt.code,
3065
+ value: opt.code
3066
+ }, toDisplayString(opt.label), 9, _hoisted_12$1);
3067
+ }), 128))], 8, _hoisted_10$1), [[vModelSelect, selectedCountry.value]])]),
3068
+ createElementVNode("label", _hoisted_13$1, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.bookSwitcher.fiscalYearEndLabel")) + " ", 1), withDirectives(createElementVNode("select", {
3069
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => selectedFiscalYearEnd.value = $event),
3070
+ class: "h-8 px-2 rounded border border-gray-300 text-sm bg-white",
3071
+ "data-testid": "accounting-settings-fiscal-year-end",
3072
+ disabled: updating.value
3073
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(fiscalYearEndOptions.value, (opt) => {
3074
+ return openBlock(), createElementBlock("option", {
3075
+ key: opt.value,
3076
+ value: opt.value
3077
+ }, toDisplayString(opt.label), 9, _hoisted_15);
3078
+ }), 128))], 8, _hoisted_14$1), [[vModelSelect, selectedFiscalYearEnd.value]])]),
3079
+ createElementVNode("p", _hoisted_16, toDisplayString(unref(t)("pluginAccounting.settings.fiscalYearEndExplain")), 1),
3080
+ updateOk.value ? (openBlock(), createElementBlock("p", _hoisted_17, toDisplayString(updateOk.value), 1)) : createCommentVNode("", true),
3081
+ updateError.value ? (openBlock(), createElementBlock("p", _hoisted_18, toDisplayString(updateError.value), 1)) : createCommentVNode("", true),
3082
+ createElementVNode("div", null, [createElementVNode("button", {
3083
+ class: "h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50",
3084
+ disabled: updating.value || !hasPendingChanges.value,
3085
+ "data-testid": "accounting-settings-save",
3086
+ onClick: onSaveBookInfo
3087
+ }, toDisplayString(updating.value ? unref(t)("pluginAccounting.common.loading") : unref(t)("pluginAccounting.settings.saveChanges")), 9, _hoisted_19)])
3088
+ ]),
3089
+ createElementVNode("section", _hoisted_20, [
3090
+ createElementVNode("h4", _hoisted_21, toDisplayString(unref(t)("pluginAccounting.settings.rebuild")), 1),
3091
+ createElementVNode("p", _hoisted_22, toDisplayString(unref(t)("pluginAccounting.settings.rebuildExplain")), 1),
3092
+ rebuildOk.value ? (openBlock(), createElementBlock("p", _hoisted_23, toDisplayString(rebuildOk.value), 1)) : createCommentVNode("", true),
3093
+ rebuildError.value ? (openBlock(), createElementBlock("p", _hoisted_24, toDisplayString(rebuildError.value), 1)) : createCommentVNode("", true),
3094
+ createElementVNode("div", null, [createElementVNode("button", {
3095
+ class: "h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50",
3096
+ disabled: rebuilding.value,
3097
+ "data-testid": "accounting-settings-rebuild",
3098
+ onClick: onRebuild
3099
+ }, toDisplayString(rebuilding.value ? unref(t)("pluginAccounting.common.loading") : unref(t)("pluginAccounting.settings.rebuild")), 9, _hoisted_25)])
3100
+ ]),
3101
+ !showAdvanced.value ? (openBlock(), createElementBlock("div", _hoisted_26, [createElementVNode("button", {
3102
+ type: "button",
3103
+ 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",
3104
+ "data-testid": "accounting-settings-advanced",
3105
+ onClick: _cache[3] || (_cache[3] = ($event) => showAdvanced.value = true)
3106
+ }, [_cache[5] || (_cache[5] = createElementVNode("span", { class: "material-icons text-base" }, "expand_more", -1)), createElementVNode("span", null, toDisplayString(unref(t)("pluginAccounting.settings.advanced")), 1)])])) : createCommentVNode("", true),
3107
+ showAdvanced.value ? (openBlock(), createElementBlock("section", _hoisted_27, [
3108
+ createElementVNode("h4", _hoisted_28, toDisplayString(unref(t)("pluginAccounting.settings.deleteBook")), 1),
3109
+ createElementVNode("p", _hoisted_29, toDisplayString(unref(t)("pluginAccounting.settings.deleteBookExplain")), 1),
3110
+ deleteError.value ? (openBlock(), createElementBlock("p", _hoisted_30, toDisplayString(deleteError.value), 1)) : createCommentVNode("", true),
3111
+ createElementVNode("label", _hoisted_31, [createTextVNode(toDisplayString(unref(t)("pluginAccounting.settings.deleteBookConfirm", { bookName: __props.bookName })) + " ", 1), withDirectives(createElementVNode("input", {
3112
+ "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => confirmName.value = $event),
3113
+ class: "h-8 px-2 rounded border border-gray-300 text-sm",
3114
+ "data-testid": "accounting-settings-delete-confirm"
3115
+ }, null, 512), [[vModelText, confirmName.value]])]),
3116
+ createElementVNode("div", null, [createElementVNode("button", {
3117
+ class: "h-8 px-3 rounded bg-red-600 hover:bg-red-700 text-white text-sm disabled:opacity-50",
3118
+ disabled: confirmName.value !== __props.bookName || deleting.value,
3119
+ "data-testid": "accounting-settings-delete",
3120
+ onClick: onDelete
3121
+ }, toDisplayString(deleting.value ? unref(t)("pluginAccounting.common.loading") : unref(t)("pluginAccounting.settings.deleteBookButton")), 9, _hoisted_32)])
3122
+ ])) : createCommentVNode("", true)
3123
+ ]);
3124
+ };
3125
+ }
3126
+ });
3127
+ //#endregion
3128
+ //#region src/vue/useAccountingChannel.ts
3129
+ function useAccountingChannel(bookId, onPayload) {
3130
+ const version = ref(0);
3131
+ let unsubscribe = null;
3132
+ function bind(nextBookId) {
3133
+ unsubscribe?.();
3134
+ unsubscribe = null;
3135
+ version.value = 0;
3136
+ if (!nextBookId) return;
3137
+ unsubscribe = hostSubscribe(bookChannel(nextBookId), (data) => {
3138
+ const event = data;
3139
+ version.value += 1;
3140
+ onPayload?.(event);
3141
+ });
3142
+ }
3143
+ watch(bookId, bind, { immediate: true });
3144
+ onUnmounted(() => {
3145
+ unsubscribe?.();
3146
+ unsubscribe = null;
3147
+ });
3148
+ return { version };
3149
+ }
3150
+ /** Subscribe to "the list of books changed" events. Use in
3151
+ * BookSwitcher.vue to refetch the dropdown contents when a sibling
3152
+ * tab adds / deletes a book. */
3153
+ function useAccountingBooksChannel(onChange) {
3154
+ const unsubscribe = hostSubscribe(ACCOUNTING_BOOKS_CHANNEL, onChange);
3155
+ onUnmounted(() => unsubscribe());
3156
+ }
3157
+ //#endregion
3158
+ //#region src/vue/View.vue?vue&type=script&setup=true&lang.ts
3159
+ var _hoisted_1$1 = {
3160
+ class: "h-full bg-white flex flex-col",
3161
+ "data-testid": "accounting-app"
3162
+ };
3163
+ var _hoisted_2 = { class: "flex items-center justify-between gap-2 px-3 py-2 border-b border-gray-100 shrink-0" };
3164
+ var _hoisted_3 = { class: "flex items-center gap-2 min-w-0" };
3165
+ var _hoisted_4 = { class: "text-lg font-semibold text-gray-800" };
3166
+ var _hoisted_5 = {
3167
+ class: "flex items-center gap-0.5 px-3 py-1.5 border-b border-gray-100 shrink-0 overflow-x-auto",
3168
+ "data-testid": "accounting-tabs"
3169
+ };
3170
+ var _hoisted_6 = [
3171
+ "data-testid",
3172
+ "disabled",
3173
+ "onClick"
3174
+ ];
3175
+ var _hoisted_7 = { class: "material-icons text-base" };
3176
+ var _hoisted_8 = { class: "flex-1 overflow-auto p-4" };
3177
+ var _hoisted_9 = {
3178
+ key: 0,
3179
+ class: "text-center text-sm text-gray-600 flex flex-col gap-2 items-center justify-center h-full",
3180
+ "data-testid": "accounting-deleted-notice"
3181
+ };
3182
+ var _hoisted_10 = {
3183
+ class: "font-medium",
3184
+ "data-testid": "accounting-deleted-notice-title"
3185
+ };
3186
+ var _hoisted_11 = { class: "text-xs text-gray-500" };
3187
+ var _hoisted_12 = {
3188
+ key: 1,
3189
+ class: "text-sm text-gray-400"
3190
+ };
3191
+ var _hoisted_13 = {
3192
+ key: 2,
3193
+ class: "text-sm text-red-500",
3194
+ "data-testid": "accounting-load-error"
3195
+ };
3196
+ var _hoisted_14 = {
3197
+ key: 3,
3198
+ class: "text-sm text-gray-500",
3199
+ "data-testid": "accounting-no-book"
3200
+ };
3201
+ //#endregion
3202
+ //#region src/vue/View.vue
3203
+ var View_default = /* @__PURE__ */ defineComponent({
3204
+ __name: "View",
3205
+ props: { selectedResult: {} },
3206
+ setup(__props) {
3207
+ const { t } = useI18n();
3208
+ const props = __props;
3209
+ const TAB_KEYS = [
3210
+ "journal",
3211
+ "opening",
3212
+ "accounts",
3213
+ "ledger",
3214
+ "balanceSheet",
3215
+ "profitLoss",
3216
+ "settings"
3217
+ ];
3218
+ const TABS = [
3219
+ {
3220
+ key: "journal",
3221
+ icon: "list",
3222
+ labelKey: "pluginAccounting.tabs.journal"
3223
+ },
3224
+ {
3225
+ key: "opening",
3226
+ icon: "play_arrow",
3227
+ labelKey: "pluginAccounting.tabs.opening"
3228
+ },
3229
+ {
3230
+ key: "accounts",
3231
+ icon: "list_alt",
3232
+ labelKey: "pluginAccounting.tabs.accounts"
3233
+ },
3234
+ {
3235
+ key: "ledger",
3236
+ icon: "menu_book",
3237
+ labelKey: "pluginAccounting.tabs.ledger"
3238
+ },
3239
+ {
3240
+ key: "balanceSheet",
3241
+ icon: "balance",
3242
+ labelKey: "pluginAccounting.tabs.balanceSheet"
3243
+ },
3244
+ {
3245
+ key: "profitLoss",
3246
+ icon: "trending_up",
3247
+ labelKey: "pluginAccounting.tabs.profitLoss"
3248
+ },
3249
+ {
3250
+ key: "settings",
3251
+ icon: "settings",
3252
+ labelKey: "pluginAccounting.tabs.settings"
3253
+ }
3254
+ ];
3255
+ function isTabKey(value) {
3256
+ return typeof value === "string" && TAB_KEYS.includes(value);
3257
+ }
3258
+ const initialPayload = computed(() => props.selectedResult?.data ?? props.selectedResult?.jsonData ?? {});
3259
+ const currentTab = ref(computed(() => isTabKey(initialPayload.value.initialTab) ? initialPayload.value.initialTab : "journal").value);
3260
+ const books = ref([]);
3261
+ const activeBookId = ref(null);
3262
+ const accounts = ref([]);
3263
+ const loadingBooks = ref(true);
3264
+ const initialLoadDone = ref(false);
3265
+ const showFirstRunForm = ref(false);
3266
+ const firstRunHandled = ref(false);
3267
+ const bookLoadError = ref(null);
3268
+ const hasOpening = ref(null);
3269
+ const activeOpeningDate = ref(void 0);
3270
+ const deletedNoticeName = ref(null);
3271
+ const activeBook = computed(() => books.value.find((book) => book.id === activeBookId.value) ?? null);
3272
+ const activeBookName = computed(() => activeBook.value?.name ?? "");
3273
+ const activeCurrency = computed(() => activeBook.value?.currency ?? "USD");
3274
+ const activeCountry = computed(() => activeBook.value?.country);
3275
+ const activeFiscalYearEnd = computed(() => activeBook.value?.fiscalYearEnd);
3276
+ const { version: bookVersion } = useAccountingChannel(activeBookId);
3277
+ useAccountingBooksChannel(() => void refetchBooks());
3278
+ function pickInitialBookId() {
3279
+ if (books.value.length === 0) return null;
3280
+ const requested = initialPayload.value.bookId;
3281
+ if (requested && books.value.some((book) => book.id === requested)) return requested;
3282
+ return books.value[0].id;
3283
+ }
3284
+ async function refetchBooks() {
3285
+ loadingBooks.value = true;
3286
+ bookLoadError.value = null;
3287
+ const previousActive = activeBook.value;
3288
+ try {
3289
+ const result = await getBooks();
3290
+ if (!result.ok) {
3291
+ bookLoadError.value = result.error;
3292
+ return;
3293
+ }
3294
+ books.value = result.data.books;
3295
+ initialLoadDone.value = true;
3296
+ if (deletedNoticeName.value === null) {
3297
+ if (!(activeBookId.value !== null && books.value.some((book) => book.id === activeBookId.value))) if (previousActive) {
3298
+ activeBookId.value = null;
3299
+ deletedNoticeName.value = previousActive.name;
3300
+ } else activeBookId.value = pickInitialBookId();
3301
+ }
3302
+ if (!firstRunHandled.value && books.value.length === 0) {
3303
+ firstRunHandled.value = true;
3304
+ showFirstRunForm.value = true;
3305
+ }
3306
+ } catch (err) {
3307
+ bookLoadError.value = errorMessage(err);
3308
+ } finally {
3309
+ loadingBooks.value = false;
3310
+ }
3311
+ }
3312
+ async function onFirstBookCreated(book) {
3313
+ showFirstRunForm.value = false;
3314
+ await refetchBooks();
3315
+ activeBookId.value = book.id;
3316
+ }
3317
+ async function onBookCreated(book) {
3318
+ if (!books.value.some((existing) => existing.id === book.id)) books.value = [...books.value, book];
3319
+ activeBookId.value = book.id;
3320
+ deletedNoticeName.value = null;
3321
+ currentTab.value = "journal";
3322
+ await refetchBooks();
3323
+ }
3324
+ async function refetchAccounts() {
3325
+ if (!activeBookId.value) {
3326
+ accounts.value = [];
3327
+ return;
3328
+ }
3329
+ const result = await getAccounts(activeBookId.value);
3330
+ if (!result.ok) return;
3331
+ accounts.value = result.data.accounts;
3332
+ }
3333
+ async function refetchOpening() {
3334
+ if (!activeBookId.value) {
3335
+ hasOpening.value = null;
3336
+ activeOpeningDate.value = void 0;
3337
+ return;
3338
+ }
3339
+ const result = await getOpeningBalances(activeBookId.value);
3340
+ if (!result.ok) return;
3341
+ hasOpening.value = result.data.opening !== null;
3342
+ activeOpeningDate.value = result.data.opening?.date;
3343
+ }
3344
+ const openingGateActive = computed(() => activeBookId.value !== null && hasOpening.value === false);
3345
+ const visibleTabs = computed(() => {
3346
+ if (openingGateActive.value) return TABS.filter((tab) => tab.key === "opening" || tab.key === "settings");
3347
+ return TABS.filter((tab) => tab.key !== "opening" || currentTab.value === "opening");
3348
+ });
3349
+ function onBookSelected(bookId) {
3350
+ activeBookId.value = bookId;
3351
+ deletedNoticeName.value = null;
3352
+ }
3353
+ const journalPreselectEntryId = ref(void 0);
3354
+ const ledgerPreselectAccountCode = ref(void 0);
3355
+ function onAccountSelected(code) {
3356
+ ledgerPreselectAccountCode.value = void 0;
3357
+ Promise.resolve().then(() => {
3358
+ ledgerPreselectAccountCode.value = code;
3359
+ });
3360
+ currentTab.value = "ledger";
3361
+ }
3362
+ function onEntrySubmitted() {
3363
+ if (currentTab.value === "opening") currentTab.value = "journal";
3364
+ }
3365
+ async function onBookDeleted(deletedName) {
3366
+ currentTab.value = "journal";
3367
+ activeBookId.value = null;
3368
+ deletedNoticeName.value = deletedName;
3369
+ await refetchBooks();
3370
+ }
3371
+ watch(() => [activeBookId.value, bookVersion.value], () => {
3372
+ if (activeBookId.value) refetchAccounts();
3373
+ }, { immediate: true });
3374
+ watch(activeBookId, () => {
3375
+ ledgerPreselectAccountCode.value = void 0;
3376
+ });
3377
+ const pendingTargetBookId = ref(null);
3378
+ function applyTargetBookId(target) {
3379
+ if (books.value.some((book) => book.id === target)) {
3380
+ activeBookId.value = target;
3381
+ pendingTargetBookId.value = null;
3382
+ return;
3383
+ }
3384
+ pendingTargetBookId.value = target;
3385
+ }
3386
+ watch(() => initialPayload.value.bookId, (next) => {
3387
+ if (!next) return;
3388
+ applyTargetBookId(next);
3389
+ });
3390
+ watch(books, () => {
3391
+ const pending = pendingTargetBookId.value;
3392
+ if (pending) applyTargetBookId(pending);
3393
+ });
3394
+ function pickTabForAction(payload) {
3395
+ if (isTabKey(payload.initialTab)) return payload.initialTab;
3396
+ switch (payload.action) {
3397
+ case ACCOUNTING_ACTIONS.addEntries:
3398
+ case ACCOUNTING_ACTIONS.voidEntry: return "journal";
3399
+ case ACCOUNTING_ACTIONS.upsertAccount: return "accounts";
3400
+ case ACCOUNTING_ACTIONS.updateBook: return "settings";
3401
+ case ACCOUNTING_ACTIONS.openBook:
3402
+ case ACCOUNTING_ACTIONS.createBook:
3403
+ case ACCOUNTING_ACTIONS.setOpeningBalances: return "balanceSheet";
3404
+ default: return null;
3405
+ }
3406
+ }
3407
+ function pickJournalPreselectId(payload) {
3408
+ if (payload.action === ACCOUNTING_ACTIONS.addEntries) {
3409
+ const entries = Array.isArray(payload.entries) ? payload.entries : [];
3410
+ return entries[entries.length - 1]?.id;
3411
+ }
3412
+ if (payload.action === ACCOUNTING_ACTIONS.voidEntry) return payload.markerEntry?.id;
3413
+ }
3414
+ watch(() => initialPayload.value, (payload) => {
3415
+ const targetTab = pickTabForAction(payload);
3416
+ if (targetTab) currentTab.value = targetTab;
3417
+ journalPreselectEntryId.value = pickJournalPreselectId(payload);
3418
+ }, { immediate: true });
3419
+ watch(activeBookId, (_next, prev) => {
3420
+ if (!prev) return;
3421
+ journalPreselectEntryId.value = void 0;
3422
+ });
3423
+ watch(() => [activeBookId.value, bookVersion.value], () => void refetchOpening(), { immediate: true });
3424
+ watch(openingGateActive, (active) => {
3425
+ if (!active) return;
3426
+ if (currentTab.value === "opening") return;
3427
+ currentTab.value = "opening";
3428
+ });
3429
+ refetchBooks();
3430
+ return (_ctx, _cache) => {
3431
+ return openBlock(), createElementBlock("div", _hoisted_1$1, [showFirstRunForm.value ? (openBlock(), createBlock(NewBookForm_default, {
3432
+ key: 0,
3433
+ "first-run": "",
3434
+ "full-page": "",
3435
+ onCreated: onFirstBookCreated
3436
+ })) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
3437
+ createElementVNode("header", _hoisted_2, [createElementVNode("div", _hoisted_3, [_cache[2] || (_cache[2] = createElementVNode("span", { class: "material-icons text-gray-600" }, "account_balance", -1)), createElementVNode("h2", _hoisted_4, toDisplayString(unref(t)("pluginAccounting.title")), 1)]), initialLoadDone.value ? (openBlock(), createBlock(BookSwitcher_default, {
3438
+ key: 0,
3439
+ "model-value": activeBookId.value ?? "",
3440
+ books: books.value,
3441
+ "onUpdate:modelValue": onBookSelected,
3442
+ onBooksChanged: refetchBooks,
3443
+ onBookCreated
3444
+ }, null, 8, ["model-value", "books"])) : createCommentVNode("", true)]),
3445
+ createElementVNode("nav", _hoisted_5, [(openBlock(true), createElementBlock(Fragment, null, renderList(visibleTabs.value, (tab) => {
3446
+ return openBlock(), createElementBlock("button", {
3447
+ key: tab.key,
3448
+ class: normalizeClass(["h-8 px-2.5 flex items-center gap-1 rounded text-sm whitespace-nowrap", deletedNoticeName.value !== null ? "text-gray-400 cursor-not-allowed" : currentTab.value === tab.key ? "bg-blue-50 text-blue-600 font-medium" : "text-gray-600 hover:bg-gray-50"]),
3449
+ "data-testid": `accounting-tab-${tab.key}`,
3450
+ disabled: deletedNoticeName.value !== null,
3451
+ onClick: ($event) => currentTab.value = tab.key
3452
+ }, [createElementVNode("span", _hoisted_7, toDisplayString(tab.icon), 1), createElementVNode("span", null, toDisplayString(unref(t)(tab.labelKey)), 1)], 10, _hoisted_6);
3453
+ }), 128))]),
3454
+ createElementVNode("main", _hoisted_8, [deletedNoticeName.value !== null ? (openBlock(), createElementBlock("div", _hoisted_9, [
3455
+ _cache[3] || (_cache[3] = createElementVNode("span", {
3456
+ class: "material-icons text-gray-400",
3457
+ style: { "font-size": "48px" }
3458
+ }, "delete_outline", -1)),
3459
+ createElementVNode("p", _hoisted_10, toDisplayString(unref(t)("pluginAccounting.deletedNotice.title", { bookName: deletedNoticeName.value })), 1),
3460
+ createElementVNode("p", _hoisted_11, toDisplayString(unref(t)("pluginAccounting.deletedNotice.body")), 1)
3461
+ ])) : loadingBooks.value && !initialLoadDone.value ? (openBlock(), createElementBlock("p", _hoisted_12, toDisplayString(unref(t)("pluginAccounting.common.loading")), 1)) : bookLoadError.value ? (openBlock(), createElementBlock("p", _hoisted_13, toDisplayString(unref(t)("pluginAccounting.common.error", { error: bookLoadError.value })), 1)) : !activeBookId.value ? (openBlock(), createElementBlock("p", _hoisted_14, toDisplayString(unref(t)("pluginAccounting.noBook")), 1)) : activeBookId.value ? (openBlock(), createElementBlock(Fragment, { key: 4 }, [currentTab.value === "journal" ? (openBlock(), createBlock(JournalList_default, {
3462
+ key: 0,
3463
+ "book-id": activeBookId.value,
3464
+ accounts: accounts.value,
3465
+ currency: activeCurrency.value,
3466
+ country: activeCountry.value,
3467
+ version: unref(bookVersion),
3468
+ "fiscal-year-end": activeFiscalYearEnd.value,
3469
+ "opening-date": activeOpeningDate.value,
3470
+ "preselect-entry-id": journalPreselectEntryId.value,
3471
+ onEditOpening: _cache[0] || (_cache[0] = ($event) => currentTab.value = "opening"),
3472
+ onPreselectConsumed: _cache[1] || (_cache[1] = ($event) => journalPreselectEntryId.value = void 0)
3473
+ }, null, 8, [
3474
+ "book-id",
3475
+ "accounts",
3476
+ "currency",
3477
+ "country",
3478
+ "version",
3479
+ "fiscal-year-end",
3480
+ "opening-date",
3481
+ "preselect-entry-id"
3482
+ ])) : currentTab.value === "opening" ? (openBlock(), createBlock(OpeningBalancesForm_default, {
3483
+ key: 1,
3484
+ "book-id": activeBookId.value,
3485
+ accounts: accounts.value,
3486
+ currency: activeCurrency.value,
3487
+ version: unref(bookVersion),
3488
+ onSubmitted: onEntrySubmitted
3489
+ }, null, 8, [
3490
+ "book-id",
3491
+ "accounts",
3492
+ "currency",
3493
+ "version"
3494
+ ])) : currentTab.value === "accounts" ? (openBlock(), createBlock(AccountsList_default, {
3495
+ key: 2,
3496
+ "book-id": activeBookId.value,
3497
+ accounts: accounts.value,
3498
+ onSelectAccount: onAccountSelected
3499
+ }, null, 8, ["book-id", "accounts"])) : currentTab.value === "ledger" ? (openBlock(), createBlock(Ledger_default, {
3500
+ key: 3,
3501
+ "book-id": activeBookId.value,
3502
+ accounts: accounts.value,
3503
+ currency: activeCurrency.value,
3504
+ version: unref(bookVersion),
3505
+ "fiscal-year-end": activeFiscalYearEnd.value,
3506
+ "opening-date": activeOpeningDate.value,
3507
+ "preselect-account-code": ledgerPreselectAccountCode.value
3508
+ }, null, 8, [
3509
+ "book-id",
3510
+ "accounts",
3511
+ "currency",
3512
+ "version",
3513
+ "fiscal-year-end",
3514
+ "opening-date",
3515
+ "preselect-account-code"
3516
+ ])) : currentTab.value === "balanceSheet" ? (openBlock(), createBlock(BalanceSheet_default, {
3517
+ key: 4,
3518
+ "book-id": activeBookId.value,
3519
+ currency: activeCurrency.value,
3520
+ version: unref(bookVersion),
3521
+ onSelectAccount: onAccountSelected
3522
+ }, null, 8, [
3523
+ "book-id",
3524
+ "currency",
3525
+ "version"
3526
+ ])) : currentTab.value === "profitLoss" ? (openBlock(), createBlock(ProfitLoss_default, {
3527
+ key: 5,
3528
+ "book-id": activeBookId.value,
3529
+ currency: activeCurrency.value,
3530
+ version: unref(bookVersion),
3531
+ "fiscal-year-end": activeFiscalYearEnd.value,
3532
+ "opening-date": activeOpeningDate.value,
3533
+ onSelectAccount: onAccountSelected
3534
+ }, null, 8, [
3535
+ "book-id",
3536
+ "currency",
3537
+ "version",
3538
+ "fiscal-year-end",
3539
+ "opening-date"
3540
+ ])) : currentTab.value === "settings" ? (openBlock(), createBlock(BookSettings_default, {
3541
+ key: 6,
3542
+ "book-id": activeBookId.value,
3543
+ "book-name": activeBookName.value,
3544
+ currency: activeCurrency.value,
3545
+ country: activeCountry.value,
3546
+ "fiscal-year-end": activeFiscalYearEnd.value,
3547
+ onDeleted: onBookDeleted,
3548
+ onBooksChanged: refetchBooks
3549
+ }, null, 8, [
3550
+ "book-id",
3551
+ "book-name",
3552
+ "currency",
3553
+ "country",
3554
+ "fiscal-year-end"
3555
+ ])) : createCommentVNode("", true)], 64)) : createCommentVNode("", true)])
3556
+ ], 64))]);
3557
+ };
3558
+ }
3559
+ });
3560
+ //#endregion
3561
+ //#region src/vue/Preview.vue?vue&type=script&setup=true&lang.ts
3562
+ var _hoisted_1 = {
3563
+ class: "text-sm text-gray-700",
3564
+ "data-testid": "accounting-preview"
3565
+ };
3566
+ //#endregion
3567
+ //#region src/vue/Preview.vue
3568
+ var Preview_default = /* @__PURE__ */ defineComponent({
3569
+ __name: "Preview",
3570
+ props: {
3571
+ data: {},
3572
+ jsonData: {}
3573
+ },
3574
+ setup(__props) {
3575
+ const { t } = useI18n();
3576
+ const props = __props;
3577
+ function summariseError(json) {
3578
+ const { error } = json;
3579
+ if (typeof error !== "string") return null;
3580
+ return t("pluginAccounting.previewError", { error });
3581
+ }
3582
+ function summariseEntry(json) {
3583
+ const { entries } = json;
3584
+ if (!Array.isArray(entries) || entries.length === 0) return null;
3585
+ const [first] = entries;
3586
+ if (!first?.id || !first?.date) return null;
3587
+ return t("pluginAccounting.preview.entry", { date: first.date });
3588
+ }
3589
+ function summarisePl(json) {
3590
+ const { profitLoss } = json;
3591
+ if (!profitLoss || typeof profitLoss.netIncome !== "number") return null;
3592
+ return t("pluginAccounting.preview.pl", {
3593
+ from: profitLoss.from ?? "?",
3594
+ to: profitLoss.to ?? "?",
3595
+ net: formatAmountNumeric(profitLoss.netIncome)
3596
+ });
3597
+ }
3598
+ function summariseBs(json) {
3599
+ const { balanceSheet } = json;
3600
+ if (!balanceSheet?.asOf || !balanceSheet.sections) return null;
3601
+ const assets = balanceSheet.sections.find((section) => section.type === "asset");
3602
+ return t("pluginAccounting.preview.bs", {
3603
+ date: balanceSheet.asOf,
3604
+ assets: assets ? formatAmountNumeric(assets.total ?? 0) : "?"
3605
+ });
3606
+ }
3607
+ function summariseBook(json) {
3608
+ const { book } = json;
3609
+ if (!book?.id || !book?.name) return null;
3610
+ return t("pluginAccounting.preview.bookCreated", {
3611
+ name: book.name,
3612
+ id: book.id
3613
+ });
3614
+ }
3615
+ function summariseFallback(json) {
3616
+ const { bookId } = json;
3617
+ if (typeof bookId === "string") return t("pluginAccounting.previewSummary", { bookId });
3618
+ return t("pluginAccounting.previewGeneric");
3619
+ }
3620
+ function asObject(value) {
3621
+ return value && typeof value === "object" ? value : {};
3622
+ }
3623
+ const summary = computed(() => {
3624
+ const json = {
3625
+ ...asObject(props.data),
3626
+ ...asObject(props.jsonData)
3627
+ };
3628
+ return summariseError(json) ?? summariseEntry(json) ?? summarisePl(json) ?? summariseBs(json) ?? summariseBook(json) ?? summariseFallback(json);
3629
+ });
3630
+ return (_ctx, _cache) => {
3631
+ return openBlock(), createElementBlock("div", _hoisted_1, [_cache[0] || (_cache[0] = createElementVNode("span", { class: "material-icons text-base align-middle mr-1" }, "account_balance", -1)), createElementVNode("span", null, toDisplayString(summary.value), 1)]);
3632
+ };
3633
+ }
3634
+ });
3635
+ //#endregion
3636
+ export { Preview_default as AccountingPreview, View_default as AccountingView, configureAccountingHost };
3637
+
3638
+ //# sourceMappingURL=vue.js.map