@rubydogjp/openkk-client-usecases 26.1.12 → 26.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/dist/assist/assist-state-helpers.d.ts +27 -0
  3. package/dist/assist/assist-state-helpers.d.ts.map +1 -0
  4. package/dist/assist/assist-state-helpers.js +183 -0
  5. package/dist/assist/assist-state-helpers.js.map +1 -0
  6. package/dist/assist/assist-state.d.ts +4 -11
  7. package/dist/assist/assist-state.d.ts.map +1 -1
  8. package/dist/assist/assist-state.js +233 -303
  9. package/dist/assist/assist-state.js.map +1 -1
  10. package/dist/entries/account-options.d.ts +11 -0
  11. package/dist/entries/account-options.d.ts.map +1 -0
  12. package/dist/entries/account-options.js +41 -0
  13. package/dist/entries/account-options.js.map +1 -0
  14. package/dist/entries/entries-state.d.ts +9 -14
  15. package/dist/entries/entries-state.d.ts.map +1 -1
  16. package/dist/entries/entries-state.js +245 -159
  17. package/dist/entries/entries-state.js.map +1 -1
  18. package/dist/entries/entry-record-state.d.ts +6 -0
  19. package/dist/entries/entry-record-state.d.ts.map +1 -0
  20. package/dist/entries/entry-record-state.js +23 -0
  21. package/dist/entries/entry-record-state.js.map +1 -0
  22. package/dist/entries/import-mapping.d.ts +12 -29
  23. package/dist/entries/import-mapping.d.ts.map +1 -1
  24. package/dist/entries/import-mapping.js +23 -74
  25. package/dist/entries/import-mapping.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/shared/archive-data-policy.d.ts +6 -0
  31. package/dist/shared/archive-data-policy.d.ts.map +1 -0
  32. package/dist/shared/archive-data-policy.js +4 -0
  33. package/dist/shared/archive-data-policy.js.map +1 -0
  34. package/dist/shared/async-mutation-queue.d.ts +9 -0
  35. package/dist/shared/async-mutation-queue.d.ts.map +1 -0
  36. package/dist/shared/async-mutation-queue.js +21 -0
  37. package/dist/shared/async-mutation-queue.js.map +1 -0
  38. package/dist/shared/async-state-version.d.ts +13 -0
  39. package/dist/shared/async-state-version.d.ts.map +1 -0
  40. package/dist/shared/async-state-version.js +27 -0
  41. package/dist/shared/async-state-version.js.map +1 -0
  42. package/dist/shared/auth-operation-guard.d.ts +3 -0
  43. package/dist/shared/auth-operation-guard.d.ts.map +1 -0
  44. package/dist/shared/auth-operation-guard.js +13 -0
  45. package/dist/shared/auth-operation-guard.js.map +1 -0
  46. package/dist/shared/backend-api-context.d.ts.map +1 -1
  47. package/dist/shared/backend-api-context.js +1 -0
  48. package/dist/shared/backend-api-context.js.map +1 -1
  49. package/dist/shared/brand-config-context.js +1 -1
  50. package/dist/shared/brand-config-context.js.map +1 -1
  51. package/dist/shared/browser-storage.d.ts +9 -0
  52. package/dist/shared/browser-storage.d.ts.map +1 -0
  53. package/dist/shared/browser-storage.js +79 -0
  54. package/dist/shared/browser-storage.js.map +1 -0
  55. package/dist/shared/callouts-context.d.ts +1 -1
  56. package/dist/shared/callouts-context.d.ts.map +1 -1
  57. package/dist/shared/callouts-context.js +5 -2
  58. package/dist/shared/callouts-context.js.map +1 -1
  59. package/dist/shared/closing-state.d.ts +2 -1
  60. package/dist/shared/closing-state.d.ts.map +1 -1
  61. package/dist/shared/closing-state.js +21 -6
  62. package/dist/shared/closing-state.js.map +1 -1
  63. package/dist/shared/editing-policy.d.ts +3 -0
  64. package/dist/shared/editing-policy.d.ts.map +1 -0
  65. package/dist/shared/editing-policy.js +13 -0
  66. package/dist/shared/editing-policy.js.map +1 -0
  67. package/dist/shared/fiscal-period-list.d.ts +5 -0
  68. package/dist/shared/fiscal-period-list.d.ts.map +1 -0
  69. package/dist/shared/fiscal-period-list.js +50 -0
  70. package/dist/shared/fiscal-period-list.js.map +1 -0
  71. package/dist/shared/maintenance-state.d.ts.map +1 -1
  72. package/dist/shared/maintenance-state.js +15 -2
  73. package/dist/shared/maintenance-state.js.map +1 -1
  74. package/dist/shared/openkk-app-state.d.ts +14 -27
  75. package/dist/shared/openkk-app-state.d.ts.map +1 -1
  76. package/dist/shared/openkk-app-state.js +240 -171
  77. package/dist/shared/openkk-app-state.js.map +1 -1
  78. package/dist/shared/openkk-config-context.d.ts +1 -0
  79. package/dist/shared/openkk-config-context.d.ts.map +1 -1
  80. package/dist/shared/openkk-config-context.js +35 -2
  81. package/dist/shared/openkk-config-context.js.map +1 -1
  82. package/dist/shared/print-adapter-context.d.ts.map +1 -1
  83. package/dist/shared/print-adapter-context.js +1 -0
  84. package/dist/shared/print-adapter-context.js.map +1 -1
  85. package/package.json +4 -4
@@ -1,64 +1,107 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { createContext, useContext, useEffect, useMemo, useState, } from "react";
3
+ import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
4
4
  import { useOpenkkAppState } from "../shared/openkk-app-state.js";
5
5
  import { useBackendApi } from "../shared/backend-api-context.js";
6
- import { useOpenkkConfig } from "../shared/openkk-config-context.js";
7
- import { parseAmount, parseBusinessRate, parseIsoLocalDate, AppError, computeStraightLineDepreciation, } from "@rubydogjp/openkk-client-domain";
6
+ import { useOpenkkConfig, useOpenkkToday, } from "../shared/openkk-config-context.js";
7
+ import { assertEditingUnlocked } from "../shared/editing-policy.js";
8
+ import { isSelectedFiscalPeriodDataPurged } from "../shared/archive-data-policy.js";
9
+ import { KeyedAsyncStateVersion } from "../shared/async-state-version.js";
10
+ import { KeyedAsyncMutationQueue } from "../shared/async-mutation-queue.js";
11
+ import { buildOpeningJournalLines, fixedAssetDraftToPatch, listFixedAssetsForPeriod, mapFixedAsset, mapOpeningJournalToRecord, nextOpeningCarryoverId, fixedAssetDraftBusinessRate, resolveFixedAssetAccountId, upsertFixedAsset, } from "./assist-state-helpers.js";
12
+ import { capFixedAssetPreviewDate, draftBusinessRate, parseAmount, AppError, } from "@rubydogjp/openkk-client-domain";
8
13
  const AssistContext = createContext(null);
9
14
  export function OpenkkAssistProvider(props) {
10
15
  const backendApi = useBackendApi();
11
16
  const appState = useOpenkkAppState();
12
17
  const config = useOpenkkConfig();
18
+ const today = useOpenkkToday();
13
19
  const [fixedAssets, setFixedAssets] = useState([]);
14
20
  const [bookAccountNameById, setBookAccountNameById] = useState({});
15
- const [bookAccountIdByName, setBookAccountIdByName] = useState({});
21
+ const [bookAccounts, setBookAccounts] = useState([]);
16
22
  const [bookAccountTypeById, setBookAccountTypeById] = useState({});
17
- const [loadError, setLoadError] = useState(null);
23
+ const [taxCategories, setTaxCategories] = useState([]);
24
+ const [taxCategoryNameById, setTaxCategoryNameById] = useState({});
25
+ const [businessCategories, setBusinessCategories] = useState([]);
26
+ const [businessCategoryNameById, setBusinessCategoryNameById] = useState({});
27
+ const [masterLoadError, setMasterLoadError] = useState(null);
28
+ const [fixedAssetsLoadError, setFixedAssetsLoadError] = useState(null);
18
29
  const [reloadNonce, setReloadNonce] = useState(0);
30
+ const sessionUserId = appState.session?.user.id ?? null;
31
+ const selectedFiscalPeriodId = useRef(appState.currentFiscalPeriodId);
32
+ const periodVersions = useRef(new KeyedAsyncStateVersion());
33
+ const assetMutationQueue = useRef(new KeyedAsyncMutationQueue());
34
+ selectedFiscalPeriodId.current = appState.currentFiscalPeriodId;
35
+ const currentFiscalPeriod = appState.fiscalPeriods.find((period) => period.id === appState.currentFiscalPeriodId);
36
+ const currentFiscalPeriodEndDate = currentFiscalPeriod?.endDate ?? null;
37
+ const currentFiscalPeriodDataPurged = isSelectedFiscalPeriodDataPurged(appState.fiscalPeriods, appState.currentFiscalPeriodId);
38
+ const fixedAssetPreviewAsOf = useMemo(() => capFixedAssetPreviewDate(today, currentFiscalPeriodEndDate), [today, currentFiscalPeriodEndDate]);
19
39
  useEffect(() => {
40
+ if (sessionUserId == null) {
41
+ setMasterLoadError(null);
42
+ return;
43
+ }
20
44
  let cancelled = false;
21
45
  void (async () => {
22
46
  try {
23
- const accounts = await backendApi.masterData.getBookAccounts();
47
+ const [accounts, taxCategories, businessCategories] = await Promise.all([
48
+ backendApi.masterData.getBookAccounts(),
49
+ backendApi.masterData.getTaxCategories(),
50
+ backendApi.masterData.getBusinessCategories(),
51
+ ]);
24
52
  if (cancelled)
25
53
  return;
26
54
  setBookAccountNameById(Object.fromEntries(accounts.map((account) => [account.id, account.name])));
27
- setBookAccountIdByName(Object.fromEntries(accounts.map((account) => [account.name, account.id])));
55
+ setBookAccounts(accounts);
28
56
  setBookAccountTypeById(Object.fromEntries(accounts.map((account) => [account.id, account.accountType])));
29
- setLoadError(null);
57
+ setTaxCategories(taxCategories);
58
+ setTaxCategoryNameById(Object.fromEntries(taxCategories.map((category) => [category.id, category.name])));
59
+ setBusinessCategories(businessCategories);
60
+ setBusinessCategoryNameById(Object.fromEntries(businessCategories.map((category) => [category.id, category.name])));
61
+ setMasterLoadError(null);
30
62
  }
31
63
  catch (error) {
32
64
  if (cancelled)
33
65
  return;
34
66
  console.error("[openkk] assist master data load failed:", error);
35
- setLoadError(error);
67
+ setMasterLoadError(error);
36
68
  }
37
69
  })();
38
70
  return () => {
39
71
  cancelled = true;
40
72
  };
41
- }, [reloadNonce]);
73
+ }, [reloadNonce, sessionUserId]);
42
74
  useEffect(() => {
43
75
  const fiscalPeriodId = appState.currentFiscalPeriodId;
44
- if (fiscalPeriodId == null || fiscalPeriodId.length === 0) {
76
+ if (fiscalPeriodId == null ||
77
+ fiscalPeriodId.length === 0 ||
78
+ currentFiscalPeriodDataPurged) {
45
79
  setFixedAssets([]);
80
+ setFixedAssetsLoadError(null);
46
81
  return;
47
82
  }
48
83
  let cancelled = false;
84
+ const authOperationVersion = appState.captureAuthOperationVersion();
85
+ const readVersion = periodVersions.current.capture(fiscalPeriodId);
49
86
  void (async () => {
50
87
  try {
51
88
  const remote = await backendApi.fixedAssets.getAll(fiscalPeriodId);
52
- if (cancelled)
89
+ if (cancelled ||
90
+ !appState.isAuthOperationCurrent(authOperationVersion) ||
91
+ !periodVersions.current.isCurrent(fiscalPeriodId, readVersion)) {
53
92
  return;
54
- setFixedAssets(replaceLoadedFixedAssets(fiscalPeriodId, remote.map((asset) => mapFixedAssetToPreview(asset, bookAccountNameById[asset.bookAccountId], config.today))));
55
- setLoadError(null);
93
+ }
94
+ setFixedAssets(remote.map((asset) => mapFixedAsset(asset, bookAccountNameById[asset.bookAccountId] ?? null, fixedAssetPreviewAsOf, currentFiscalPeriodEndDate)));
95
+ setFixedAssetsLoadError(null);
56
96
  }
57
97
  catch (error) {
58
- if (cancelled)
98
+ if (cancelled ||
99
+ !appState.isAuthOperationCurrent(authOperationVersion) ||
100
+ !periodVersions.current.isCurrent(fiscalPeriodId, readVersion)) {
59
101
  return;
102
+ }
60
103
  console.error("[openkk] fixed assets load failed:", error);
61
- setLoadError(error);
104
+ setFixedAssetsLoadError(error);
62
105
  }
63
106
  })();
64
107
  return () => {
@@ -67,74 +110,102 @@ export function OpenkkAssistProvider(props) {
67
110
  }, [
68
111
  appState.currentFiscalPeriodId,
69
112
  bookAccountNameById,
70
- config.today,
113
+ currentFiscalPeriodEndDate,
114
+ currentFiscalPeriodDataPurged,
115
+ fixedAssetPreviewAsOf,
71
116
  reloadNonce,
72
117
  ]);
73
118
  const value = useMemo(() => {
119
+ const loadError = masterLoadError ?? fixedAssetsLoadError;
74
120
  return {
75
- listFixedAssets() {
76
- return fixedAssets;
121
+ listFixedAssets(fiscalPeriodId) {
122
+ return listFixedAssetsForPeriod(fixedAssets, fiscalPeriodId);
77
123
  },
78
124
  getFixedAsset(assetId) {
79
125
  return fixedAssets.find((asset) => asset.id === assetId) ?? null;
80
126
  },
81
127
  async addFixedAsset(draft) {
128
+ assertEditingUnlocked(config.editingPolicy, "assist.addFixedAsset");
129
+ const authOperationVersion = appState.captureAuthOperationVersion();
82
130
  const fiscalPeriodId = appState.currentFiscalPeriodId;
83
- if (fiscalPeriodId == null || fiscalPeriodId.length === 0) {
131
+ if (fiscalPeriodId == null)
84
132
  return null;
85
- }
86
- const accountId = bookAccountIdByName[draft.account] ?? null;
87
- if (accountId == null || accountId.length === 0) {
133
+ periodVersions.current.invalidate(fiscalPeriodId);
134
+ const accountId = resolveFixedAssetAccountId(null, draft.account, bookAccounts);
135
+ if (accountId == null) {
88
136
  throw new AppError({
89
137
  messageForDeveloper: "assist.addFixedAsset: accountId missing",
90
138
  messageForUser: "勘定科目が解決できないため保存できませんでした",
91
139
  originalMessage: null,
92
140
  statusCode: null,
141
+ code: null,
93
142
  });
94
143
  }
95
- const created = await backendApi.fixedAssets.create(fiscalPeriodId, {
96
- name: draft.name,
97
- acquisitionDate: draft.acquisitionDate,
98
- acquisitionCost: parseAmount(draft.acquisitionCost),
99
- usefulLife: Math.max(1, Math.round(draft.usefulLife) || 1),
100
- depreciationMethod: "straight_line",
101
- businessRate: Math.max(0, Math.min(100, draft.businessRatePercent)) / 100,
102
- bookAccountId: accountId,
103
- });
104
- setFixedAssets((current) => [
105
- ...current,
106
- mapFixedAssetToPreview(created, bookAccountNameById[created.bookAccountId], config.today),
107
- ]);
108
- return created.id;
144
+ try {
145
+ const created = await backendApi.fixedAssets.create(fiscalPeriodId, {
146
+ name: draft.name,
147
+ acquisitionDate: draft.acquisitionDate,
148
+ acquisitionCost: parseAmount(draft.acquisitionCost),
149
+ usefulLife: Math.max(1, Math.round(draft.usefulLife) || 1),
150
+ depreciationMethod: "straight_line",
151
+ businessRate: fixedAssetDraftBusinessRate(draft),
152
+ bookAccountId: accountId,
153
+ });
154
+ appState.assertAuthOperationCurrent(authOperationVersion);
155
+ if (selectedFiscalPeriodId.current === fiscalPeriodId) {
156
+ const mapped = mapFixedAsset(created, bookAccountNameById[created.bookAccountId] ?? null, fixedAssetPreviewAsOf, currentFiscalPeriodEndDate ?? null);
157
+ setFixedAssets((current) => upsertFixedAsset(current, mapped));
158
+ }
159
+ return created.id;
160
+ }
161
+ finally {
162
+ periodVersions.current.invalidate(fiscalPeriodId);
163
+ }
109
164
  },
110
165
  async updateFixedAsset(assetId, draft) {
166
+ assertEditingUnlocked(config.editingPolicy, "assist.updateFixedAsset");
167
+ const authOperationVersion = appState.captureAuthOperationVersion();
111
168
  const current = fixedAssets.find((asset) => asset.id === assetId) ?? null;
112
- const fiscalPeriodId = current?.fiscalPeriodId ?? appState.currentFiscalPeriodId ?? "";
113
- if (fiscalPeriodId.length === 0)
169
+ const fiscalPeriodId = current?.fiscalPeriodId ?? appState.currentFiscalPeriodId;
170
+ if (fiscalPeriodId == null)
114
171
  return false;
115
- // ユーザーが科目名を変更した場合は draft 側を優先して解決する。
116
- const accountId = bookAccountIdByName[draft.account] ?? current?.accountId ?? null;
117
- if (accountId == null || accountId.length === 0) {
172
+ const accountId = resolveFixedAssetAccountId(current == null
173
+ ? null
174
+ : {
175
+ accountId: current.bookAccountId,
176
+ accountName: current.accountName,
177
+ }, draft.account, bookAccounts);
178
+ if (accountId == null) {
118
179
  throw new AppError({
119
180
  messageForDeveloper: "assist.updateFixedAsset: accountId missing",
120
181
  messageForUser: "勘定科目が解決できないため保存できませんでした",
121
182
  originalMessage: null,
122
183
  statusCode: null,
184
+ code: null,
123
185
  });
124
186
  }
125
- // 簿価・進捗・残期間は計算で導出するため保存しない。保存するのは
126
- // 償却計算の元になる「真実」の値(取得価額・取得日・耐用年数・事業割合)のみ。
127
- const patched = await backendApi.fixedAssets.patch(fiscalPeriodId, assetId, fixedAssetDraftToPatch(draft, accountId));
128
- setFixedAssets((currentList) => currentList.map((asset) => asset.id === assetId
129
- ? mapFixedAssetToPreview(patched, bookAccountNameById[patched.bookAccountId], config.today)
130
- : asset));
131
- return true;
187
+ return await assetMutationQueue.current.run(assetId, async () => {
188
+ appState.assertAuthOperationCurrent(authOperationVersion);
189
+ periodVersions.current.invalidate(fiscalPeriodId);
190
+ try {
191
+ const patched = await backendApi.fixedAssets.patch(fiscalPeriodId, assetId, fixedAssetDraftToPatch(draft, accountId));
192
+ appState.assertAuthOperationCurrent(authOperationVersion);
193
+ if (selectedFiscalPeriodId.current === fiscalPeriodId) {
194
+ const mapped = mapFixedAsset(patched, bookAccountNameById[patched.bookAccountId] ?? null, fixedAssetPreviewAsOf, currentFiscalPeriodEndDate ?? null);
195
+ setFixedAssets((currentList) => upsertFixedAsset(currentList, mapped));
196
+ }
197
+ return true;
198
+ }
199
+ finally {
200
+ periodVersions.current.invalidate(fiscalPeriodId);
201
+ }
202
+ });
132
203
  },
133
204
  listOpeningCarryovers(fiscalPeriodId) {
134
205
  const period = appState.fiscalPeriods.find((p) => p.id === fiscalPeriodId);
135
- const journals = period?.opening?.openingJournals ?? [];
206
+ const journals = period?.opening.journals ?? [];
136
207
  return journals
137
- .map((journal) => mapOpeningJournalToRecord(journal, fiscalPeriodId, bookAccountNameById, bookAccountTypeById))
208
+ .map((journal) => mapOpeningJournalToRecord(journal, fiscalPeriodId, bookAccountNameById, bookAccountTypeById, taxCategoryNameById, businessCategoryNameById))
138
209
  .sort((left, right) => {
139
210
  const byDate = left.date.localeCompare(right.date);
140
211
  if (byDate !== 0)
@@ -144,164 +215,133 @@ export function OpenkkAssistProvider(props) {
144
215
  },
145
216
  getOpeningCarryover(carryoverId) {
146
217
  const fiscalPeriodId = appState.currentFiscalPeriodId;
147
- if (fiscalPeriodId == null || fiscalPeriodId.length === 0)
218
+ if (fiscalPeriodId == null)
148
219
  return null;
149
220
  const period = appState.fiscalPeriods.find((p) => p.id === fiscalPeriodId);
150
- const journals = period?.opening?.openingJournals ?? [];
221
+ const journals = period?.opening.journals ?? [];
151
222
  const journal = journals.find((item) => item.id === carryoverId);
152
223
  if (journal == null)
153
224
  return null;
154
- return mapOpeningJournalToRecord(journal, fiscalPeriodId, bookAccountNameById, bookAccountTypeById);
225
+ return mapOpeningJournalToRecord(journal, fiscalPeriodId, bookAccountNameById, bookAccountTypeById, taxCategoryNameById, businessCategoryNameById);
155
226
  },
156
- async addOpeningCarryover(fiscalPeriodId) {
227
+ async addOpeningCarryover(fiscalPeriodId, draft) {
228
+ assertEditingUnlocked(config.editingPolicy, "assist.addOpeningCarryover");
157
229
  const period = appState.fiscalPeriods.find((p) => p.id === fiscalPeriodId);
158
- const opening = period?.opening;
159
- if (period == null || opening == null)
230
+ if (period == null)
160
231
  return null;
161
- const debitAccountId = bookAccountIdByName["売掛金"] ??
162
- Object.values(bookAccountIdByName)[0] ??
163
- "";
164
- const creditAccountId = bookAccountIdByName["売上"] ??
165
- Object.values(bookAccountIdByName)[0] ??
166
- "";
167
- if (debitAccountId === "" || creditAccountId === "") {
168
- throw new AppError({
169
- messageForDeveloper: "assist.addOpeningCarryover: account resolution failed",
170
- messageForUser: "勘定科目が取得できないため再振替仕訳を作成できませんでした",
171
- originalMessage: null,
172
- statusCode: null,
232
+ let nextId = null;
233
+ const updated = await appState.updateFiscalPeriod(fiscalPeriodId, (currentPeriod) => {
234
+ const currentOpening = currentPeriod.opening;
235
+ const generatedId = nextOpeningCarryoverId(fiscalPeriodId, currentOpening.journals);
236
+ nextId = generatedId;
237
+ const lines = buildOpeningJournalLines(generatedId, draft, {
238
+ accounts: bookAccounts,
239
+ taxCategories,
240
+ businessCategories,
173
241
  });
174
- }
175
- const nextId = nextOpeningCarryoverId(fiscalPeriodId, opening.openingJournals ?? []);
176
- const newJournal = {
177
- id: nextId,
178
- date: period.startDate,
179
- description: "期首再振替",
180
- businessRate: 1,
181
- lines: [
182
- {
183
- id: `${nextId}-d`,
184
- side: "debit",
185
- bookAccountId: debitAccountId,
186
- amount: 0,
187
- partnerName: "",
188
- taxCategoryId: "",
189
- businessCategoryId: "",
190
- },
191
- {
192
- id: `${nextId}-c`,
193
- side: "credit",
194
- bookAccountId: creditAccountId,
195
- amount: 0,
196
- partnerName: "",
197
- taxCategoryId: "",
198
- businessCategoryId: "",
242
+ if (lines == null) {
243
+ throw openingCarryoverAccountResolutionError("assist.addOpeningCarryover");
244
+ }
245
+ const newJournal = {
246
+ id: generatedId,
247
+ date: draft.date,
248
+ description: draft.description,
249
+ businessRate: draftBusinessRate(draft),
250
+ lines,
251
+ };
252
+ return {
253
+ opening: {
254
+ balanceLines: currentOpening.balanceLines,
255
+ journals: [
256
+ ...currentOpening.journals,
257
+ newJournal,
258
+ ],
199
259
  },
200
- ],
201
- };
202
- const updatedJournals = [
203
- ...(opening.openingJournals ?? []),
204
- newJournal,
205
- ];
206
- await appState.updateFiscalPeriod(fiscalPeriodId, {
207
- opening: {
208
- ...opening,
209
- openingJournals: updatedJournals,
210
- },
260
+ };
211
261
  });
212
- return nextId;
262
+ return updated ? nextId : null;
213
263
  },
214
264
  async updateOpeningCarryover(carryoverId, draft) {
265
+ assertEditingUnlocked(config.editingPolicy, "assist.updateOpeningCarryover");
215
266
  const fiscalPeriodId = appState.currentFiscalPeriodId;
216
- if (fiscalPeriodId == null || fiscalPeriodId.length === 0)
267
+ if (fiscalPeriodId == null)
217
268
  return false;
218
269
  const period = appState.fiscalPeriods.find((p) => p.id === fiscalPeriodId);
219
- const opening = period?.opening;
220
- if (period == null || opening == null)
270
+ if (period == null)
221
271
  return false;
222
- const debitAccountId = resolveBookAccountIdByName(draft.debit, bookAccountIdByName) ?? "";
223
- const creditAccountId = resolveBookAccountIdByName(draft.credit, bookAccountIdByName) ?? "";
224
- if (debitAccountId === "" || creditAccountId === "") {
225
- throw new AppError({
226
- messageForDeveloper: "assist.updateOpeningCarryover: account resolution failed",
227
- messageForUser: "勘定科目が解決できないため保存できませんでした",
228
- originalMessage: null,
229
- statusCode: null,
272
+ return await appState.updateFiscalPeriod(fiscalPeriodId, (currentPeriod) => {
273
+ const currentOpening = currentPeriod.opening;
274
+ const journals = currentOpening.journals;
275
+ const target = journals.find((journal) => journal.id === carryoverId);
276
+ if (target == null)
277
+ return null;
278
+ const lines = buildOpeningJournalLines(target.id, draft, {
279
+ accounts: bookAccounts,
280
+ taxCategories,
281
+ businessCategories,
230
282
  });
231
- }
232
- const journals = opening.openingJournals ?? [];
233
- let updated = false;
234
- const nextJournals = journals.map((journal) => {
235
- if (journal.id !== carryoverId)
236
- return journal;
237
- updated = true;
238
- return {
239
- ...journal,
283
+ if (lines == null) {
284
+ throw openingCarryoverAccountResolutionError("assist.updateOpeningCarryover");
285
+ }
286
+ const nextJournal = {
287
+ ...target,
240
288
  date: draft.date,
241
289
  description: draft.description,
242
- businessRate: parseBusinessRate(draft.businessRate),
243
- lines: [
244
- {
245
- id: journal.lines.find((line) => line.side === "debit")?.id ??
246
- `${journal.id}-d`,
247
- side: "debit",
248
- bookAccountId: debitAccountId,
249
- amount: parseAmount(draft.debitAmount),
250
- partnerName: draft.partner,
251
- taxCategoryId: draft.taxCategory,
252
- businessCategoryId: draft.businessCategory,
253
- },
254
- {
255
- id: journal.lines.find((line) => line.side === "credit")?.id ??
256
- `${journal.id}-c`,
257
- side: "credit",
258
- bookAccountId: creditAccountId,
259
- amount: parseAmount(draft.creditAmount),
260
- partnerName: draft.partner,
261
- taxCategoryId: draft.taxCategory,
262
- businessCategoryId: draft.businessCategory,
263
- },
264
- ],
290
+ businessRate: draftBusinessRate(draft),
291
+ lines,
292
+ };
293
+ return {
294
+ opening: {
295
+ balanceLines: currentOpening.balanceLines,
296
+ journals: journals.map((journal) => journal.id === carryoverId ? nextJournal : journal),
297
+ },
265
298
  };
266
299
  });
267
- if (!updated)
268
- return false;
269
- await appState.updateFiscalPeriod(fiscalPeriodId, {
270
- opening: {
271
- ...opening,
272
- openingJournals: nextJournals,
273
- },
274
- });
275
- return true;
276
300
  },
277
301
  async deleteFixedAsset(assetId) {
302
+ assertEditingUnlocked(config.editingPolicy, "assist.deleteFixedAsset");
303
+ const authOperationVersion = appState.captureAuthOperationVersion();
278
304
  const current = fixedAssets.find((asset) => asset.id === assetId) ?? null;
279
- const fiscalPeriodId = current?.fiscalPeriodId ?? appState.currentFiscalPeriodId ?? "";
280
- if (fiscalPeriodId.length === 0)
305
+ const fiscalPeriodId = current?.fiscalPeriodId ?? appState.currentFiscalPeriodId;
306
+ if (fiscalPeriodId == null)
281
307
  return false;
282
- await backendApi.fixedAssets.remove(fiscalPeriodId, assetId);
283
- setFixedAssets((currentList) => currentList.filter((asset) => asset.id !== assetId));
284
- return true;
308
+ return await assetMutationQueue.current.run(assetId, async () => {
309
+ appState.assertAuthOperationCurrent(authOperationVersion);
310
+ periodVersions.current.invalidate(fiscalPeriodId);
311
+ try {
312
+ await backendApi.fixedAssets.remove(fiscalPeriodId, assetId);
313
+ appState.assertAuthOperationCurrent(authOperationVersion);
314
+ if (selectedFiscalPeriodId.current === fiscalPeriodId) {
315
+ setFixedAssets((currentList) => currentList.filter((asset) => asset.id !== assetId));
316
+ }
317
+ return true;
318
+ }
319
+ finally {
320
+ periodVersions.current.invalidate(fiscalPeriodId);
321
+ }
322
+ });
285
323
  },
286
324
  async deleteOpeningCarryover(carryoverId) {
325
+ assertEditingUnlocked(config.editingPolicy, "assist.deleteOpeningCarryover");
287
326
  const fiscalPeriodId = appState.currentFiscalPeriodId;
288
- if (fiscalPeriodId == null || fiscalPeriodId.length === 0)
327
+ if (fiscalPeriodId == null)
289
328
  return false;
290
329
  const period = appState.fiscalPeriods.find((p) => p.id === fiscalPeriodId);
291
- const opening = period?.opening;
292
- if (period == null || opening == null)
293
- return false;
294
- const journals = opening.openingJournals ?? [];
295
- const nextJournals = journals.filter((journal) => journal.id !== carryoverId);
296
- if (nextJournals.length === journals.length)
330
+ if (period == null)
297
331
  return false;
298
- await appState.updateFiscalPeriod(fiscalPeriodId, {
299
- opening: {
300
- ...opening,
301
- openingJournals: nextJournals,
302
- },
332
+ return await appState.updateFiscalPeriod(fiscalPeriodId, (currentPeriod) => {
333
+ const currentOpening = currentPeriod.opening;
334
+ const journals = currentOpening.journals;
335
+ const nextJournals = journals.filter((journal) => journal.id !== carryoverId);
336
+ if (nextJournals.length === journals.length)
337
+ return null;
338
+ return {
339
+ opening: {
340
+ balanceLines: currentOpening.balanceLines,
341
+ journals: nextJournals,
342
+ },
343
+ };
303
344
  });
304
- return true;
305
345
  },
306
346
  loadError,
307
347
  reload() {
@@ -311,140 +351,29 @@ export function OpenkkAssistProvider(props) {
311
351
  }, [
312
352
  appState.currentFiscalPeriodId,
313
353
  appState.fiscalPeriods,
314
- bookAccountIdByName,
354
+ bookAccounts,
315
355
  bookAccountNameById,
316
356
  bookAccountTypeById,
317
- config.today,
357
+ businessCategories,
358
+ businessCategoryNameById,
359
+ currentFiscalPeriodEndDate,
360
+ fixedAssetPreviewAsOf,
318
361
  fixedAssets,
319
- loadError,
362
+ fixedAssetsLoadError,
363
+ masterLoadError,
364
+ taxCategories,
365
+ taxCategoryNameById,
320
366
  ]);
321
367
  return (_jsx(AssistContext.Provider, { value: value, children: props.children }));
322
368
  }
323
- export function replaceLoadedFixedAssets(fiscalPeriodId, nextAssets) {
324
- if (fiscalPeriodId == null || fiscalPeriodId.length === 0)
325
- return [];
326
- return nextAssets;
327
- }
328
- export function nextOpeningCarryoverId(fiscalPeriodId, journals) {
329
- const prefix = `oc-${fiscalPeriodId}-`;
330
- const maxSuffix = journals.reduce((max, journal) => {
331
- if (!journal.id.startsWith(prefix))
332
- return max;
333
- const suffix = Number(journal.id.slice(prefix.length));
334
- return Number.isInteger(suffix) && suffix > max ? suffix : max;
335
- }, 0);
336
- return `${prefix}${maxSuffix + 1}`;
337
- }
338
- function mapOpeningJournalToRecord(journal, fiscalPeriodId, accountNameById, accountTypeById) {
339
- const debit = journal.lines.find((line) => line.side === "debit");
340
- const credit = journal.lines.find((line) => line.side === "credit");
341
- return {
342
- id: journal.id,
343
- fiscalPeriodId,
344
- date: journal.date,
345
- description: journal.description,
346
- debit: accountNameById[debit?.bookAccountId ?? ""] ?? debit?.bookAccountId ?? "",
347
- debitType: accountTypeById[debit?.bookAccountId ?? ""] ?? "asset",
348
- debitAmount: formatAmount(debit?.amount ?? 0),
349
- credit: accountNameById[credit?.bookAccountId ?? ""] ??
350
- credit?.bookAccountId ??
351
- "",
352
- creditType: accountTypeById[credit?.bookAccountId ?? ""] ?? "revenue",
353
- creditAmount: formatAmount(credit?.amount ?? 0),
354
- partner: debit?.partnerName ?? credit?.partnerName ?? "",
355
- taxCategory: debit?.taxCategoryId ?? credit?.taxCategoryId ?? "対象外",
356
- businessCategory: debit?.businessCategoryId ?? credit?.businessCategoryId ?? "対象外",
357
- businessRate: String(Math.round((journal.businessRate ?? 1) * 100)),
358
- debitBookAccountId: debit?.bookAccountId,
359
- creditBookAccountId: credit?.bookAccountId,
360
- };
361
- }
362
- function resolveBookAccountIdByName(name, accountIdByName) {
363
- const id = accountIdByName[name];
364
- return id == null || id.length === 0 ? null : id;
365
- }
366
- function formatAmount(value) {
367
- return new Intl.NumberFormat("ja-JP").format(Math.abs(value));
368
- }
369
- function mapFixedAssetToPreview(asset, accountName, today) {
370
- // 売却・廃棄・除却済みは処分日(無ければ今日)で償却を打ち切る。
371
- const isClosed = asset.status !== "active";
372
- const asOf = isClosed && asset.disposalDate
373
- ? (parseIsoLocalDate(asset.disposalDate) ?? today)
374
- : today;
375
- const depreciation = computeStraightLineDepreciation({
376
- acquisitionDate: asset.acquisitionDate,
377
- acquisitionCost: asset.acquisitionCost,
378
- usefulLife: asset.usefulLife,
379
- asOf,
369
+ function openingCarryoverAccountResolutionError(operation) {
370
+ return new AppError({
371
+ messageForDeveloper: `${operation}: account resolution failed`,
372
+ messageForUser: "勘定科目が解決できないため保存できませんでした",
373
+ originalMessage: null,
374
+ statusCode: null,
375
+ code: null,
380
376
  });
381
- return {
382
- id: asset.id,
383
- fiscalPeriodId: asset.fiscalPeriodId,
384
- name: asset.name,
385
- account: accountName ?? asset.bookAccountId,
386
- accountId: asset.bookAccountId,
387
- period: depreciation.periodLabel,
388
- remaining: depreciation.remainingLabel,
389
- progress: depreciation.progress,
390
- current: formatYen(depreciation.currentBookValue),
391
- purchase: formatYen(asset.acquisitionCost),
392
- status: mapFixedAssetStatusLabel(asset.status),
393
- depreciationAmount: formatYen(depreciation.annualDepreciation),
394
- acquisitionDate: asset.acquisitionDate,
395
- acquisitionCost: asset.acquisitionCost,
396
- usefulLife: asset.usefulLife,
397
- businessRate: asset.businessRate,
398
- disposalDate: asset.disposalDate || undefined,
399
- disposalPrice: asset.disposalPrice
400
- ? formatYen(asset.disposalPrice)
401
- : undefined,
402
- };
403
- }
404
- function formatYen(value) {
405
- return new Intl.NumberFormat("ja-JP").format(value);
406
- }
407
- function mapFixedAssetStatusLabel(status) {
408
- if (status === "active")
409
- return "償却中";
410
- if (status === "sold")
411
- return "売却済";
412
- if (status === "disposed")
413
- return "廃棄済";
414
- if (status === "retired")
415
- return "完了";
416
- return status;
417
- }
418
- function mapFixedAssetStatusApi(statusLabel) {
419
- if (statusLabel === "償却中")
420
- return "active";
421
- if (statusLabel === "売却済")
422
- return "sold";
423
- if (statusLabel === "廃棄済")
424
- return "disposed";
425
- if (statusLabel === "完了")
426
- return "retired";
427
- return "active";
428
- }
429
- export function fixedAssetDraftToPatch(draft, bookAccountId) {
430
- return {
431
- name: draft.name,
432
- acquisitionDate: draft.acquisitionDate,
433
- acquisitionCost: parseAmount(draft.acquisitionCost),
434
- usefulLife: Math.max(1, Math.round(draft.usefulLife) || 1),
435
- businessRate: Math.max(0, Math.min(100, draft.businessRatePercent)) / 100,
436
- status: mapFixedAssetStatusApi(draft.status),
437
- disposalDate: isFixedAssetClosedStatus(draft.status)
438
- ? (draft.disposalDate ?? "")
439
- : "",
440
- disposalPrice: draft.status === "売却済" ? parseAmount(draft.disposalPrice ?? "0") : 0,
441
- bookAccountId,
442
- };
443
- }
444
- function isFixedAssetClosedStatus(statusLabel) {
445
- return (statusLabel === "売却済" ||
446
- statusLabel === "廃棄済" ||
447
- statusLabel === "完了");
448
377
  }
449
378
  export function useOpenkkAssist() {
450
379
  const value = useContext(AssistContext);
@@ -454,6 +383,7 @@ export function useOpenkkAssist() {
454
383
  messageForUser: "補助データを読み込めませんでした",
455
384
  originalMessage: null,
456
385
  statusCode: null,
386
+ code: null,
457
387
  });
458
388
  }
459
389
  return value;