@rubydogjp/openkk-client-usecases 26.1.12 → 26.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +227 -302
  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 +239 -158
  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,38 +1,65 @@
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 selectedFiscalPeriodId = useRef(appState.currentFiscalPeriodId);
31
+ const periodVersions = useRef(new KeyedAsyncStateVersion());
32
+ const assetMutationQueue = useRef(new KeyedAsyncMutationQueue());
33
+ selectedFiscalPeriodId.current = appState.currentFiscalPeriodId;
34
+ const currentFiscalPeriod = appState.fiscalPeriods.find((period) => period.id === appState.currentFiscalPeriodId);
35
+ const currentFiscalPeriodEndDate = currentFiscalPeriod?.endDate ?? null;
36
+ const currentFiscalPeriodDataPurged = isSelectedFiscalPeriodDataPurged(appState.fiscalPeriods, appState.currentFiscalPeriodId);
37
+ const fixedAssetPreviewAsOf = useMemo(() => capFixedAssetPreviewDate(today, currentFiscalPeriodEndDate), [today, currentFiscalPeriodEndDate]);
19
38
  useEffect(() => {
20
39
  let cancelled = false;
21
40
  void (async () => {
22
41
  try {
23
- const accounts = await backendApi.masterData.getBookAccounts();
42
+ const [accounts, taxCategories, businessCategories] = await Promise.all([
43
+ backendApi.masterData.getBookAccounts(),
44
+ backendApi.masterData.getTaxCategories(),
45
+ backendApi.masterData.getBusinessCategories(),
46
+ ]);
24
47
  if (cancelled)
25
48
  return;
26
49
  setBookAccountNameById(Object.fromEntries(accounts.map((account) => [account.id, account.name])));
27
- setBookAccountIdByName(Object.fromEntries(accounts.map((account) => [account.name, account.id])));
50
+ setBookAccounts(accounts);
28
51
  setBookAccountTypeById(Object.fromEntries(accounts.map((account) => [account.id, account.accountType])));
29
- setLoadError(null);
52
+ setTaxCategories(taxCategories);
53
+ setTaxCategoryNameById(Object.fromEntries(taxCategories.map((category) => [category.id, category.name])));
54
+ setBusinessCategories(businessCategories);
55
+ setBusinessCategoryNameById(Object.fromEntries(businessCategories.map((category) => [category.id, category.name])));
56
+ setMasterLoadError(null);
30
57
  }
31
58
  catch (error) {
32
59
  if (cancelled)
33
60
  return;
34
61
  console.error("[openkk] assist master data load failed:", error);
35
- setLoadError(error);
62
+ setMasterLoadError(error);
36
63
  }
37
64
  })();
38
65
  return () => {
@@ -41,24 +68,35 @@ export function OpenkkAssistProvider(props) {
41
68
  }, [reloadNonce]);
42
69
  useEffect(() => {
43
70
  const fiscalPeriodId = appState.currentFiscalPeriodId;
44
- if (fiscalPeriodId == null || fiscalPeriodId.length === 0) {
71
+ if (fiscalPeriodId == null ||
72
+ fiscalPeriodId.length === 0 ||
73
+ currentFiscalPeriodDataPurged) {
45
74
  setFixedAssets([]);
75
+ setFixedAssetsLoadError(null);
46
76
  return;
47
77
  }
48
78
  let cancelled = false;
79
+ const authOperationVersion = appState.captureAuthOperationVersion();
80
+ const readVersion = periodVersions.current.capture(fiscalPeriodId);
49
81
  void (async () => {
50
82
  try {
51
83
  const remote = await backendApi.fixedAssets.getAll(fiscalPeriodId);
52
- if (cancelled)
84
+ if (cancelled ||
85
+ !appState.isAuthOperationCurrent(authOperationVersion) ||
86
+ !periodVersions.current.isCurrent(fiscalPeriodId, readVersion)) {
53
87
  return;
54
- setFixedAssets(replaceLoadedFixedAssets(fiscalPeriodId, remote.map((asset) => mapFixedAssetToPreview(asset, bookAccountNameById[asset.bookAccountId], config.today))));
55
- setLoadError(null);
88
+ }
89
+ setFixedAssets(remote.map((asset) => mapFixedAsset(asset, bookAccountNameById[asset.bookAccountId] ?? null, fixedAssetPreviewAsOf, currentFiscalPeriodEndDate)));
90
+ setFixedAssetsLoadError(null);
56
91
  }
57
92
  catch (error) {
58
- if (cancelled)
93
+ if (cancelled ||
94
+ !appState.isAuthOperationCurrent(authOperationVersion) ||
95
+ !periodVersions.current.isCurrent(fiscalPeriodId, readVersion)) {
59
96
  return;
97
+ }
60
98
  console.error("[openkk] fixed assets load failed:", error);
61
- setLoadError(error);
99
+ setFixedAssetsLoadError(error);
62
100
  }
63
101
  })();
64
102
  return () => {
@@ -67,74 +105,102 @@ export function OpenkkAssistProvider(props) {
67
105
  }, [
68
106
  appState.currentFiscalPeriodId,
69
107
  bookAccountNameById,
70
- config.today,
108
+ currentFiscalPeriodEndDate,
109
+ currentFiscalPeriodDataPurged,
110
+ fixedAssetPreviewAsOf,
71
111
  reloadNonce,
72
112
  ]);
73
113
  const value = useMemo(() => {
114
+ const loadError = masterLoadError ?? fixedAssetsLoadError;
74
115
  return {
75
- listFixedAssets() {
76
- return fixedAssets;
116
+ listFixedAssets(fiscalPeriodId) {
117
+ return listFixedAssetsForPeriod(fixedAssets, fiscalPeriodId);
77
118
  },
78
119
  getFixedAsset(assetId) {
79
120
  return fixedAssets.find((asset) => asset.id === assetId) ?? null;
80
121
  },
81
122
  async addFixedAsset(draft) {
123
+ assertEditingUnlocked(config.editingPolicy, "assist.addFixedAsset");
124
+ const authOperationVersion = appState.captureAuthOperationVersion();
82
125
  const fiscalPeriodId = appState.currentFiscalPeriodId;
83
- if (fiscalPeriodId == null || fiscalPeriodId.length === 0) {
126
+ if (fiscalPeriodId == null)
84
127
  return null;
85
- }
86
- const accountId = bookAccountIdByName[draft.account] ?? null;
87
- if (accountId == null || accountId.length === 0) {
128
+ periodVersions.current.invalidate(fiscalPeriodId);
129
+ const accountId = resolveFixedAssetAccountId(null, draft.account, bookAccounts);
130
+ if (accountId == null) {
88
131
  throw new AppError({
89
132
  messageForDeveloper: "assist.addFixedAsset: accountId missing",
90
133
  messageForUser: "勘定科目が解決できないため保存できませんでした",
91
134
  originalMessage: null,
92
135
  statusCode: null,
136
+ code: null,
93
137
  });
94
138
  }
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;
139
+ try {
140
+ const created = await backendApi.fixedAssets.create(fiscalPeriodId, {
141
+ name: draft.name,
142
+ acquisitionDate: draft.acquisitionDate,
143
+ acquisitionCost: parseAmount(draft.acquisitionCost),
144
+ usefulLife: Math.max(1, Math.round(draft.usefulLife) || 1),
145
+ depreciationMethod: "straight_line",
146
+ businessRate: fixedAssetDraftBusinessRate(draft),
147
+ bookAccountId: accountId,
148
+ });
149
+ appState.assertAuthOperationCurrent(authOperationVersion);
150
+ if (selectedFiscalPeriodId.current === fiscalPeriodId) {
151
+ const mapped = mapFixedAsset(created, bookAccountNameById[created.bookAccountId] ?? null, fixedAssetPreviewAsOf, currentFiscalPeriodEndDate ?? null);
152
+ setFixedAssets((current) => upsertFixedAsset(current, mapped));
153
+ }
154
+ return created.id;
155
+ }
156
+ finally {
157
+ periodVersions.current.invalidate(fiscalPeriodId);
158
+ }
109
159
  },
110
160
  async updateFixedAsset(assetId, draft) {
161
+ assertEditingUnlocked(config.editingPolicy, "assist.updateFixedAsset");
162
+ const authOperationVersion = appState.captureAuthOperationVersion();
111
163
  const current = fixedAssets.find((asset) => asset.id === assetId) ?? null;
112
- const fiscalPeriodId = current?.fiscalPeriodId ?? appState.currentFiscalPeriodId ?? "";
113
- if (fiscalPeriodId.length === 0)
164
+ const fiscalPeriodId = current?.fiscalPeriodId ?? appState.currentFiscalPeriodId;
165
+ if (fiscalPeriodId == null)
114
166
  return false;
115
- // ユーザーが科目名を変更した場合は draft 側を優先して解決する。
116
- const accountId = bookAccountIdByName[draft.account] ?? current?.accountId ?? null;
117
- if (accountId == null || accountId.length === 0) {
167
+ const accountId = resolveFixedAssetAccountId(current == null
168
+ ? null
169
+ : {
170
+ accountId: current.bookAccountId,
171
+ accountName: current.accountName,
172
+ }, draft.account, bookAccounts);
173
+ if (accountId == null) {
118
174
  throw new AppError({
119
175
  messageForDeveloper: "assist.updateFixedAsset: accountId missing",
120
176
  messageForUser: "勘定科目が解決できないため保存できませんでした",
121
177
  originalMessage: null,
122
178
  statusCode: null,
179
+ code: null,
123
180
  });
124
181
  }
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;
182
+ return await assetMutationQueue.current.run(assetId, async () => {
183
+ appState.assertAuthOperationCurrent(authOperationVersion);
184
+ periodVersions.current.invalidate(fiscalPeriodId);
185
+ try {
186
+ const patched = await backendApi.fixedAssets.patch(fiscalPeriodId, assetId, fixedAssetDraftToPatch(draft, accountId));
187
+ appState.assertAuthOperationCurrent(authOperationVersion);
188
+ if (selectedFiscalPeriodId.current === fiscalPeriodId) {
189
+ const mapped = mapFixedAsset(patched, bookAccountNameById[patched.bookAccountId] ?? null, fixedAssetPreviewAsOf, currentFiscalPeriodEndDate ?? null);
190
+ setFixedAssets((currentList) => upsertFixedAsset(currentList, mapped));
191
+ }
192
+ return true;
193
+ }
194
+ finally {
195
+ periodVersions.current.invalidate(fiscalPeriodId);
196
+ }
197
+ });
132
198
  },
133
199
  listOpeningCarryovers(fiscalPeriodId) {
134
200
  const period = appState.fiscalPeriods.find((p) => p.id === fiscalPeriodId);
135
- const journals = period?.opening?.openingJournals ?? [];
201
+ const journals = period?.opening.journals ?? [];
136
202
  return journals
137
- .map((journal) => mapOpeningJournalToRecord(journal, fiscalPeriodId, bookAccountNameById, bookAccountTypeById))
203
+ .map((journal) => mapOpeningJournalToRecord(journal, fiscalPeriodId, bookAccountNameById, bookAccountTypeById, taxCategoryNameById, businessCategoryNameById))
138
204
  .sort((left, right) => {
139
205
  const byDate = left.date.localeCompare(right.date);
140
206
  if (byDate !== 0)
@@ -144,164 +210,133 @@ export function OpenkkAssistProvider(props) {
144
210
  },
145
211
  getOpeningCarryover(carryoverId) {
146
212
  const fiscalPeriodId = appState.currentFiscalPeriodId;
147
- if (fiscalPeriodId == null || fiscalPeriodId.length === 0)
213
+ if (fiscalPeriodId == null)
148
214
  return null;
149
215
  const period = appState.fiscalPeriods.find((p) => p.id === fiscalPeriodId);
150
- const journals = period?.opening?.openingJournals ?? [];
216
+ const journals = period?.opening.journals ?? [];
151
217
  const journal = journals.find((item) => item.id === carryoverId);
152
218
  if (journal == null)
153
219
  return null;
154
- return mapOpeningJournalToRecord(journal, fiscalPeriodId, bookAccountNameById, bookAccountTypeById);
220
+ return mapOpeningJournalToRecord(journal, fiscalPeriodId, bookAccountNameById, bookAccountTypeById, taxCategoryNameById, businessCategoryNameById);
155
221
  },
156
- async addOpeningCarryover(fiscalPeriodId) {
222
+ async addOpeningCarryover(fiscalPeriodId, draft) {
223
+ assertEditingUnlocked(config.editingPolicy, "assist.addOpeningCarryover");
157
224
  const period = appState.fiscalPeriods.find((p) => p.id === fiscalPeriodId);
158
- const opening = period?.opening;
159
- if (period == null || opening == null)
225
+ if (period == null)
160
226
  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,
227
+ let nextId = null;
228
+ const updated = await appState.updateFiscalPeriod(fiscalPeriodId, (currentPeriod) => {
229
+ const currentOpening = currentPeriod.opening;
230
+ const generatedId = nextOpeningCarryoverId(fiscalPeriodId, currentOpening.journals);
231
+ nextId = generatedId;
232
+ const lines = buildOpeningJournalLines(generatedId, draft, {
233
+ accounts: bookAccounts,
234
+ taxCategories,
235
+ businessCategories,
173
236
  });
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: "",
237
+ if (lines == null) {
238
+ throw openingCarryoverAccountResolutionError("assist.addOpeningCarryover");
239
+ }
240
+ const newJournal = {
241
+ id: generatedId,
242
+ date: draft.date,
243
+ description: draft.description,
244
+ businessRate: draftBusinessRate(draft),
245
+ lines,
246
+ };
247
+ return {
248
+ opening: {
249
+ balanceLines: currentOpening.balanceLines,
250
+ journals: [
251
+ ...currentOpening.journals,
252
+ newJournal,
253
+ ],
199
254
  },
200
- ],
201
- };
202
- const updatedJournals = [
203
- ...(opening.openingJournals ?? []),
204
- newJournal,
205
- ];
206
- await appState.updateFiscalPeriod(fiscalPeriodId, {
207
- opening: {
208
- ...opening,
209
- openingJournals: updatedJournals,
210
- },
255
+ };
211
256
  });
212
- return nextId;
257
+ return updated ? nextId : null;
213
258
  },
214
259
  async updateOpeningCarryover(carryoverId, draft) {
260
+ assertEditingUnlocked(config.editingPolicy, "assist.updateOpeningCarryover");
215
261
  const fiscalPeriodId = appState.currentFiscalPeriodId;
216
- if (fiscalPeriodId == null || fiscalPeriodId.length === 0)
262
+ if (fiscalPeriodId == null)
217
263
  return false;
218
264
  const period = appState.fiscalPeriods.find((p) => p.id === fiscalPeriodId);
219
- const opening = period?.opening;
220
- if (period == null || opening == null)
265
+ if (period == null)
221
266
  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,
267
+ return await appState.updateFiscalPeriod(fiscalPeriodId, (currentPeriod) => {
268
+ const currentOpening = currentPeriod.opening;
269
+ const journals = currentOpening.journals;
270
+ const target = journals.find((journal) => journal.id === carryoverId);
271
+ if (target == null)
272
+ return null;
273
+ const lines = buildOpeningJournalLines(target.id, draft, {
274
+ accounts: bookAccounts,
275
+ taxCategories,
276
+ businessCategories,
230
277
  });
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,
278
+ if (lines == null) {
279
+ throw openingCarryoverAccountResolutionError("assist.updateOpeningCarryover");
280
+ }
281
+ const nextJournal = {
282
+ ...target,
240
283
  date: draft.date,
241
284
  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
- ],
285
+ businessRate: draftBusinessRate(draft),
286
+ lines,
287
+ };
288
+ return {
289
+ opening: {
290
+ balanceLines: currentOpening.balanceLines,
291
+ journals: journals.map((journal) => journal.id === carryoverId ? nextJournal : journal),
292
+ },
265
293
  };
266
294
  });
267
- if (!updated)
268
- return false;
269
- await appState.updateFiscalPeriod(fiscalPeriodId, {
270
- opening: {
271
- ...opening,
272
- openingJournals: nextJournals,
273
- },
274
- });
275
- return true;
276
295
  },
277
296
  async deleteFixedAsset(assetId) {
297
+ assertEditingUnlocked(config.editingPolicy, "assist.deleteFixedAsset");
298
+ const authOperationVersion = appState.captureAuthOperationVersion();
278
299
  const current = fixedAssets.find((asset) => asset.id === assetId) ?? null;
279
- const fiscalPeriodId = current?.fiscalPeriodId ?? appState.currentFiscalPeriodId ?? "";
280
- if (fiscalPeriodId.length === 0)
300
+ const fiscalPeriodId = current?.fiscalPeriodId ?? appState.currentFiscalPeriodId;
301
+ if (fiscalPeriodId == null)
281
302
  return false;
282
- await backendApi.fixedAssets.remove(fiscalPeriodId, assetId);
283
- setFixedAssets((currentList) => currentList.filter((asset) => asset.id !== assetId));
284
- return true;
303
+ return await assetMutationQueue.current.run(assetId, async () => {
304
+ appState.assertAuthOperationCurrent(authOperationVersion);
305
+ periodVersions.current.invalidate(fiscalPeriodId);
306
+ try {
307
+ await backendApi.fixedAssets.remove(fiscalPeriodId, assetId);
308
+ appState.assertAuthOperationCurrent(authOperationVersion);
309
+ if (selectedFiscalPeriodId.current === fiscalPeriodId) {
310
+ setFixedAssets((currentList) => currentList.filter((asset) => asset.id !== assetId));
311
+ }
312
+ return true;
313
+ }
314
+ finally {
315
+ periodVersions.current.invalidate(fiscalPeriodId);
316
+ }
317
+ });
285
318
  },
286
319
  async deleteOpeningCarryover(carryoverId) {
320
+ assertEditingUnlocked(config.editingPolicy, "assist.deleteOpeningCarryover");
287
321
  const fiscalPeriodId = appState.currentFiscalPeriodId;
288
- if (fiscalPeriodId == null || fiscalPeriodId.length === 0)
322
+ if (fiscalPeriodId == null)
289
323
  return false;
290
324
  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)
325
+ if (period == null)
297
326
  return false;
298
- await appState.updateFiscalPeriod(fiscalPeriodId, {
299
- opening: {
300
- ...opening,
301
- openingJournals: nextJournals,
302
- },
327
+ return await appState.updateFiscalPeriod(fiscalPeriodId, (currentPeriod) => {
328
+ const currentOpening = currentPeriod.opening;
329
+ const journals = currentOpening.journals;
330
+ const nextJournals = journals.filter((journal) => journal.id !== carryoverId);
331
+ if (nextJournals.length === journals.length)
332
+ return null;
333
+ return {
334
+ opening: {
335
+ balanceLines: currentOpening.balanceLines,
336
+ journals: nextJournals,
337
+ },
338
+ };
303
339
  });
304
- return true;
305
340
  },
306
341
  loadError,
307
342
  reload() {
@@ -311,140 +346,29 @@ export function OpenkkAssistProvider(props) {
311
346
  }, [
312
347
  appState.currentFiscalPeriodId,
313
348
  appState.fiscalPeriods,
314
- bookAccountIdByName,
349
+ bookAccounts,
315
350
  bookAccountNameById,
316
351
  bookAccountTypeById,
317
- config.today,
352
+ businessCategories,
353
+ businessCategoryNameById,
354
+ currentFiscalPeriodEndDate,
355
+ fixedAssetPreviewAsOf,
318
356
  fixedAssets,
319
- loadError,
357
+ fixedAssetsLoadError,
358
+ masterLoadError,
359
+ taxCategories,
360
+ taxCategoryNameById,
320
361
  ]);
321
362
  return (_jsx(AssistContext.Provider, { value: value, children: props.children }));
322
363
  }
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,
364
+ function openingCarryoverAccountResolutionError(operation) {
365
+ return new AppError({
366
+ messageForDeveloper: `${operation}: account resolution failed`,
367
+ messageForUser: "勘定科目が解決できないため保存できませんでした",
368
+ originalMessage: null,
369
+ statusCode: null,
370
+ code: null,
380
371
  });
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
372
  }
449
373
  export function useOpenkkAssist() {
450
374
  const value = useContext(AssistContext);
@@ -454,6 +378,7 @@ export function useOpenkkAssist() {
454
378
  messageForUser: "補助データを読み込めませんでした",
455
379
  originalMessage: null,
456
380
  statusCode: null,
381
+ code: null,
457
382
  });
458
383
  }
459
384
  return value;