@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.
- package/dist/server/accountNormalize.d.ts +3 -0
- package/dist/server/accountNormalize.d.ts.map +1 -0
- package/dist/server/atomic.d.ts +13 -0
- package/dist/server/atomic.d.ts.map +1 -0
- package/dist/server/context.d.ts +39 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/defaultAccounts.d.ts +3 -0
- package/dist/server/defaultAccounts.d.ts.map +1 -0
- package/dist/server/eventPublisher.d.ts +14 -0
- package/dist/server/eventPublisher.d.ts.map +1 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/io.d.ts +67 -0
- package/dist/server/io.d.ts.map +1 -0
- package/dist/server/journal.d.ts +74 -0
- package/dist/server/journal.d.ts.map +1 -0
- package/dist/server/openingBalances.d.ts +30 -0
- package/dist/server/openingBalances.d.ts.map +1 -0
- package/dist/server/report.d.ts +98 -0
- package/dist/server/report.d.ts.map +1 -0
- package/dist/server/router.d.ts +7 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/service.d.ts +148 -0
- package/dist/server/service.d.ts.map +1 -0
- package/dist/server/snapshotCache.d.ts +52 -0
- package/dist/server/snapshotCache.d.ts.map +1 -0
- package/dist/server/timeSeries.d.ts +47 -0
- package/dist/server/timeSeries.d.ts.map +1 -0
- package/dist/server/types.d.ts +134 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server.cjs +2101 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.js +2074 -0
- package/dist/server.js.map +1 -0
- package/dist/shared/actions.d.ts +19 -0
- package/dist/shared/actions.d.ts.map +1 -0
- package/dist/shared/channels.d.ts +46 -0
- package/dist/shared/channels.d.ts.map +1 -0
- package/dist/shared/countries.d.ts +51 -0
- package/dist/shared/countries.d.ts.map +1 -0
- package/dist/shared/currencies.d.ts +34 -0
- package/dist/shared/currencies.d.ts.map +1 -0
- package/dist/shared/dates.d.ts +15 -0
- package/dist/shared/dates.d.ts.map +1 -0
- package/dist/shared/errors.d.ts +2 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/fiscalYear.d.ts +22 -0
- package/dist/shared/fiscalYear.d.ts.map +1 -0
- package/dist/shared/index.d.ts +9 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/timeSeriesEnums.d.ts +5 -0
- package/dist/shared/timeSeriesEnums.d.ts.map +1 -0
- package/dist/shared.cjs +466 -0
- package/dist/shared.cjs.map +1 -0
- package/dist/shared.js +432 -0
- package/dist/shared.js.map +1 -0
- package/dist/style.css +1255 -0
- package/dist/vue/Preview.vue.d.ts +8 -0
- package/dist/vue/Preview.vue.d.ts.map +1 -0
- package/dist/vue/View.vue.d.ts +30 -0
- package/dist/vue/View.vue.d.ts.map +1 -0
- package/dist/vue/api.d.ts +269 -0
- package/dist/vue/api.d.ts.map +1 -0
- package/dist/vue/components/AccountEditor.vue.d.ts +19 -0
- package/dist/vue/components/AccountEditor.vue.d.ts.map +1 -0
- package/dist/vue/components/AccountRow.vue.d.ts +14 -0
- package/dist/vue/components/AccountRow.vue.d.ts.map +1 -0
- package/dist/vue/components/AccountsList.vue.d.ts +15 -0
- package/dist/vue/components/AccountsList.vue.d.ts.map +1 -0
- package/dist/vue/components/AccountsModal.vue.d.ts +15 -0
- package/dist/vue/components/AccountsModal.vue.d.ts.map +1 -0
- package/dist/vue/components/BalanceSheet.vue.d.ts +13 -0
- package/dist/vue/components/BalanceSheet.vue.d.ts.map +1 -0
- package/dist/vue/components/BookSettings.vue.d.ts +18 -0
- package/dist/vue/components/BookSettings.vue.d.ts.map +1 -0
- package/dist/vue/components/BookSwitcher.vue.d.ts +17 -0
- package/dist/vue/components/BookSwitcher.vue.d.ts.map +1 -0
- package/dist/vue/components/DateRangePicker.vue.d.ts +19 -0
- package/dist/vue/components/DateRangePicker.vue.d.ts.map +1 -0
- package/dist/vue/components/JournalEntryForm.vue.d.ts +19 -0
- package/dist/vue/components/JournalEntryForm.vue.d.ts.map +1 -0
- package/dist/vue/components/JournalList.vue.d.ts +30 -0
- package/dist/vue/components/JournalList.vue.d.ts.map +1 -0
- package/dist/vue/components/Ledger.vue.d.ts +21 -0
- package/dist/vue/components/Ledger.vue.d.ts.map +1 -0
- package/dist/vue/components/NewBookForm.vue.d.ts +20 -0
- package/dist/vue/components/NewBookForm.vue.d.ts.map +1 -0
- package/dist/vue/components/OpeningBalancesForm.vue.d.ts +15 -0
- package/dist/vue/components/OpeningBalancesForm.vue.d.ts.map +1 -0
- package/dist/vue/components/ProfitLoss.vue.d.ts +19 -0
- package/dist/vue/components/ProfitLoss.vue.d.ts.map +1 -0
- package/dist/vue/components/accountDraft.d.ts +8 -0
- package/dist/vue/components/accountDraft.d.ts.map +1 -0
- package/dist/vue/components/accountNumbering.d.ts +20 -0
- package/dist/vue/components/accountNumbering.d.ts.map +1 -0
- package/dist/vue/components/accountValidation.d.ts +34 -0
- package/dist/vue/components/accountValidation.d.ts.map +1 -0
- package/dist/vue/components/useLatestRequest.d.ts +10 -0
- package/dist/vue/components/useLatestRequest.d.ts.map +1 -0
- package/dist/vue/hostContext.d.ts +31 -0
- package/dist/vue/hostContext.d.ts.map +1 -0
- package/dist/vue/index.d.ts +7 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/useAccountingChannel.d.ts +13 -0
- package/dist/vue/useAccountingChannel.d.ts.map +1 -0
- package/dist/vue.cjs +3641 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.js +3638 -0
- package/dist/vue.js.map +1 -0
- 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
|