@chenchaolong/plugin-trade-compliance-workbench 1.0.18 → 1.0.20
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/lib/catalog-import/catalog-import.pipeline.d.ts.map +1 -1
- package/dist/lib/catalog-import/catalog-import.pipeline.js +2 -1
- package/dist/lib/catalog-import/catalog-import.pipeline.js.map +1 -1
- package/dist/lib/catalog-import/confidence.js +2 -2
- package/dist/lib/catalog-import/confidence.js.map +1 -1
- package/dist/lib/catalog-import/contracts.d.ts +5 -2
- package/dist/lib/catalog-import/contracts.d.ts.map +1 -1
- package/dist/lib/catalog-import/outcome.d.ts +19 -0
- package/dist/lib/catalog-import/outcome.d.ts.map +1 -0
- package/dist/lib/catalog-import/outcome.js +18 -0
- package/dist/lib/catalog-import/outcome.js.map +1 -0
- package/dist/lib/catalog-import/spreadsheet-structure.d.ts +15 -0
- package/dist/lib/catalog-import/spreadsheet-structure.d.ts.map +1 -0
- package/dist/lib/catalog-import/spreadsheet-structure.js +44 -0
- package/dist/lib/catalog-import/spreadsheet-structure.js.map +1 -0
- package/dist/lib/catalog-import/spreadsheet.parser.d.ts.map +1 -1
- package/dist/lib/catalog-import/spreadsheet.parser.js +71 -0
- package/dist/lib/catalog-import/spreadsheet.parser.js.map +1 -1
- package/dist/lib/domain/contract-number.js +2 -2
- package/dist/lib/domain/contract-number.js.map +1 -1
- package/dist/lib/entities/catalog.entity.d.ts +5 -1
- package/dist/lib/entities/catalog.entity.d.ts.map +1 -1
- package/dist/lib/entities/catalog.entity.js +16 -0
- package/dist/lib/entities/catalog.entity.js.map +1 -1
- package/dist/lib/remote-components/trade-compliance-workbench/app.css +1 -1
- package/dist/lib/remote-components/trade-compliance-workbench/app.js +5 -5
- package/dist/lib/remote-ui/app-source.d.ts.map +1 -1
- package/dist/lib/remote-ui/app-source.js +208 -67
- package/dist/lib/remote-ui/app-source.js.map +1 -1
- package/dist/lib/remote-ui/components/forms.js +1 -1
- package/dist/lib/remote-ui/components/forms.js.map +1 -1
- package/dist/lib/remote-ui/components/task-status.d.ts +2 -1
- package/dist/lib/remote-ui/components/task-status.d.ts.map +1 -1
- package/dist/lib/remote-ui/components/task-status.js +5 -4
- package/dist/lib/remote-ui/components/task-status.js.map +1 -1
- package/dist/lib/remote-ui/contracts.d.ts +3 -0
- package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/import-task-status.d.ts +10 -0
- package/dist/lib/remote-ui/import-task-status.d.ts.map +1 -0
- package/dist/lib/remote-ui/import-task-status.js +236 -0
- package/dist/lib/remote-ui/import-task-status.js.map +1 -0
- package/dist/lib/remote-ui/model.d.ts.map +1 -1
- package/dist/lib/remote-ui/model.js +2 -1
- package/dist/lib/remote-ui/model.js.map +1 -1
- package/dist/lib/remote-ui/pages/analytics-prototype.css +307 -0
- package/dist/lib/remote-ui/pages/catalogs.css +681 -0
- package/dist/lib/remote-ui/pages/catalogs.d.ts +9 -3
- package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.js +36 -7
- package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
- package/dist/lib/remote-ui/pages/contracts.css +259 -0
- package/dist/lib/remote-ui/pages/contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/contracts.js +36 -13
- package/dist/lib/remote-ui/pages/contracts.js.map +1 -1
- package/dist/lib/remote-ui/pages/files-prototype.css +547 -0
- package/dist/lib/remote-ui/pages/files.d.ts +3 -2
- package/dist/lib/remote-ui/pages/files.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/files.js +15 -15
- package/dist/lib/remote-ui/pages/files.js.map +1 -1
- package/dist/lib/remote-ui/pages/procurement-contracts.css +939 -0
- package/dist/lib/remote-ui/pages/procurement.js +1 -1
- package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
- package/dist/lib/remote-ui/pages/sales-files.css +512 -0
- package/dist/lib/remote-ui/pages/sales.js +2 -2
- package/dist/lib/remote-ui/pages/sales.js.map +1 -1
- package/dist/lib/remote-ui/pages/settings.css +178 -0
- package/dist/lib/remote-ui/pages/settings.js +1 -1
- package/dist/lib/remote-ui/pages/settings.js.map +1 -1
- package/dist/lib/remote-ui/polling.d.ts +1 -1
- package/dist/lib/remote-ui/polling.d.ts.map +1 -1
- package/dist/lib/remote-ui/polling.js +4 -2
- package/dist/lib/remote-ui/polling.js.map +1 -1
- package/dist/lib/remote-ui/purchase-review-adapter.d.ts +4 -0
- package/dist/lib/remote-ui/purchase-review-adapter.d.ts.map +1 -0
- package/dist/lib/remote-ui/purchase-review-adapter.js +78 -0
- package/dist/lib/remote-ui/purchase-review-adapter.js.map +1 -0
- package/dist/lib/remote-ui/styles.css +588 -0
- package/dist/lib/sales-file.service.d.ts.map +1 -1
- package/dist/lib/sales-file.service.js +19 -7
- package/dist/lib/sales-file.service.js.map +1 -1
- package/dist/lib/types.d.ts +12 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/workbench-view.provider.d.ts +24 -0
- package/dist/lib/workbench-view.provider.d.ts.map +1 -1
- package/dist/lib/workbench-view.provider.js +19 -4
- package/dist/lib/workbench-view.provider.js.map +1 -1
- package/dist/lib/workbench.plugin.d.ts.map +1 -1
- package/dist/lib/workbench.plugin.js +1 -2
- package/dist/lib/workbench.plugin.js.map +1 -1
- package/dist/lib/workbench.service.d.ts +7 -1
- package/dist/lib/workbench.service.d.ts.map +1 -1
- package/dist/lib/workbench.service.js +276 -75
- package/dist/lib/workbench.service.js.map +1 -1
- package/package.json +4 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-source.d.ts","sourceRoot":"","sources":["../../../src/lib/remote-ui/app-source.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app-source.d.ts","sourceRoot":"","sources":["../../../src/lib/remote-ui/app-source.tsx"],"names":[],"mappings":"AA8BA,OAAO,cAAc,CAAA"}
|
|
@@ -13,12 +13,14 @@ import { buildBreadcrumbs, WorkbenchShell } from './components/layout.js';
|
|
|
13
13
|
import { CatalogCommands, CatalogImportError, ControlledGoodsPage, findActiveCatalogImportTasks, findLatestFailedCatalogImportTask, runCatalogImportOrchestration, SanctionedCompaniesPage } from './pages/catalogs.js';
|
|
14
14
|
import { HomePage } from './pages/home.js';
|
|
15
15
|
import { ProcurementPage } from './pages/procurement.js';
|
|
16
|
+
import { adaptPurchaseReview } from './purchase-review-adapter.js';
|
|
16
17
|
import { CustomerContractsPage, adaptCustomerContractReview } from './pages/contracts.js';
|
|
17
18
|
import { SalesPage } from './pages/sales.js';
|
|
18
19
|
import { SalesFilesPage } from './pages/files.js';
|
|
19
20
|
import { AnalyticsPage, AnalyticsPrototypeBoard } from './pages/analytics.js';
|
|
20
21
|
import { SettingsPage } from './pages/settings.js';
|
|
21
22
|
import { ContractImportError, findRecoverableContractImport, findResumableContractImports, runContractImportOrchestration, runCustomerContractReanalysis, taskTransition } from './contract-import.js';
|
|
23
|
+
import { importTaskFromStart, importTaskProgressPercent, isLocalUploadPlaceholder, mergeImportTaskItems } from './import-task-status.js';
|
|
22
24
|
import { compareTaskBusinessOrder, createPendingResultQueue, createPollingTaskTracker, isAbortError, isPollingObservationTimeout, submitAndRefreshTaskState } from './polling.js';
|
|
23
25
|
import { ACTION_NOTIFICATION_POLICIES, executeWorkbenchAction } from './action-execution.js';
|
|
24
26
|
import './styles.css';
|
|
@@ -34,12 +36,14 @@ const pageMeta = {
|
|
|
34
36
|
settings: { title: '系统设置', subtitle: '维护企业、银行、汇率和利润规则。' }
|
|
35
37
|
};
|
|
36
38
|
const procurementPageMeta = {
|
|
37
|
-
suppliers: { title: '供应商列表', subtitle: '
|
|
39
|
+
suppliers: { title: '供应商列表', subtitle: '管理供应商主档,并通过供应商代码进入该供应商的采购订单列表。' },
|
|
38
40
|
orders: { title: '采购订单列表', subtitle: '查看当前供应商的采购订单、金额及合规状态。' },
|
|
39
41
|
lines: { title: '采购商品列表', subtitle: '查看当前采购订单的商品明细及合规状态。' }
|
|
40
42
|
};
|
|
41
43
|
const bridge = createRemoteBridge();
|
|
42
44
|
const initialAnalyticsQuery = { mode: 'PURCHASE', dimension: 'SUPPLIER', filters: {}, page: 1, pageSize: 20 };
|
|
45
|
+
let summaryRequestSequence = 0;
|
|
46
|
+
const DOWNLOAD_URL_REVOKE_DELAY_MS = 30_000;
|
|
43
47
|
function comparePendingReviewBusinessOrder(left, right) {
|
|
44
48
|
const leftTask = left.kind === 'CONTRACT_IMPORT' ? left.completion.task : left.result.run;
|
|
45
49
|
const rightTask = right.kind === 'CONTRACT_IMPORT' ? right.completion.task : right.result.run;
|
|
@@ -72,6 +76,7 @@ function App() {
|
|
|
72
76
|
const [analyticsPrototypeVisible, setAnalyticsPrototypeVisible] = React.useState(false);
|
|
73
77
|
const [salesFilesPrototypeState, setSalesFilesPrototypeState] = React.useState(undefined);
|
|
74
78
|
const [data, setData] = React.useState(() => normalizeWorkbenchData(null));
|
|
79
|
+
const importTaskItemsRef = React.useRef([]);
|
|
75
80
|
const [busy, setBusy] = React.useState(false);
|
|
76
81
|
const [actionBusy, setActionBusy] = React.useState(false);
|
|
77
82
|
const [catalogCommand, setCatalogCommand] = React.useState(null);
|
|
@@ -106,6 +111,42 @@ function App() {
|
|
|
106
111
|
toastTimers.dismiss(id);
|
|
107
112
|
setToasts((items) => removeToast(items, id));
|
|
108
113
|
}, [toastTimers]);
|
|
114
|
+
const notifyTaskStatusActionError = React.useCallback((error) => {
|
|
115
|
+
setToast({ message: error instanceof Error ? error.message : '任务操作失败', tone: 'error' });
|
|
116
|
+
}, [setToast]);
|
|
117
|
+
const notifyTaskRecoveryRefreshError = React.useCallback((error) => {
|
|
118
|
+
setToast({ message: error instanceof Error ? `任务状态刷新失败:${error.message}` : '任务状态刷新失败', tone: 'error' });
|
|
119
|
+
}, [setToast]);
|
|
120
|
+
const handleBackgroundImportTrackingError = React.useCallback((error) => {
|
|
121
|
+
if (isAbortError(error) || isPollingObservationTimeout(error))
|
|
122
|
+
return;
|
|
123
|
+
setToast({ message: error instanceof Error ? `后台任务跟踪失败:${error.message}` : '后台任务跟踪失败', tone: 'error' });
|
|
124
|
+
}, [setToast]);
|
|
125
|
+
const upsertLocalImportTask = React.useCallback((task) => {
|
|
126
|
+
setData(current => {
|
|
127
|
+
const items = mergeImportTaskItems(current.importTasks.items, [task]);
|
|
128
|
+
return {
|
|
129
|
+
...current,
|
|
130
|
+
importTasks: {
|
|
131
|
+
...current.importTasks,
|
|
132
|
+
items,
|
|
133
|
+
total: Math.max(numberValue(current.importTasks.total, 0), items.length),
|
|
134
|
+
page: numberValue(current.importTasks.page, 1),
|
|
135
|
+
pageSize: current.importTasks.pageSize ?? 100
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}, []);
|
|
140
|
+
const loadImportTasksFallback = React.useCallback(async (normalized) => {
|
|
141
|
+
try {
|
|
142
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData(importTasksCollectionRequest())));
|
|
143
|
+
const importTasks = normalizeWorkbenchData({ summary: { importTasks: payload } }).importTasks;
|
|
144
|
+
return importTasks.items.length > 0 || numberValue(importTasks.total, 0) > 0 ? importTasks : normalized.importTasks;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return normalized.importTasks;
|
|
148
|
+
}
|
|
149
|
+
}, []);
|
|
109
150
|
const configureAnalyticsPrototypeCapability = React.useCallback((parameters) => {
|
|
110
151
|
analyticsPrototypeListenerCleanupRef.current?.();
|
|
111
152
|
analyticsPrototypeListenerCleanupRef.current = null;
|
|
@@ -188,11 +229,12 @@ function App() {
|
|
|
188
229
|
const requestId = gate.begin();
|
|
189
230
|
setBusy(true);
|
|
190
231
|
try {
|
|
191
|
-
const response = await bridge.requestData(
|
|
232
|
+
const response = await bridge.requestData(summaryRequestQuery(active.initialQuery?.parameters));
|
|
192
233
|
if (gate.isLatest(requestId)) {
|
|
193
234
|
const normalized = normalizeWorkbenchData(response);
|
|
235
|
+
const importTasks = await loadImportTasksFallback(normalized);
|
|
194
236
|
bridge.configure({ uploadPolicy: normalized.uploadPolicy });
|
|
195
|
-
setData(normalized);
|
|
237
|
+
setData(current => mergeWorkbenchDataWithLocalTasks(current, { ...normalized, importTasks }));
|
|
196
238
|
await loadRoute(routeRef.current);
|
|
197
239
|
}
|
|
198
240
|
}
|
|
@@ -204,7 +246,7 @@ function App() {
|
|
|
204
246
|
gate.finish(requestId);
|
|
205
247
|
setBusy(gate.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0);
|
|
206
248
|
}
|
|
207
|
-
}, [loadRoute]);
|
|
249
|
+
}, [loadImportTasksFallback, loadRoute]);
|
|
208
250
|
const loadTaskState = React.useCallback(async () => {
|
|
209
251
|
const active = contextRef.current;
|
|
210
252
|
if (!active)
|
|
@@ -212,17 +254,22 @@ function App() {
|
|
|
212
254
|
const gate = taskStateRequestGateRef.current;
|
|
213
255
|
const requestId = gate.begin();
|
|
214
256
|
try {
|
|
215
|
-
const response = await bridge.requestData(
|
|
257
|
+
const response = await bridge.requestData(summaryRequestQuery(active.initialQuery?.parameters));
|
|
216
258
|
if (!gate.commit(requestId))
|
|
217
259
|
return false;
|
|
218
260
|
const normalized = normalizeWorkbenchData(response);
|
|
261
|
+
const importTasks = await loadImportTasksFallback(normalized);
|
|
219
262
|
setData(current => ({
|
|
220
263
|
...current,
|
|
221
264
|
controlledGoods: normalized.controlledGoods,
|
|
222
265
|
controlledBatches: normalized.controlledBatches,
|
|
223
266
|
sanctionedCompanies: normalized.sanctionedCompanies,
|
|
224
267
|
sanctionBatches: normalized.sanctionBatches,
|
|
225
|
-
importTasks:
|
|
268
|
+
importTasks: {
|
|
269
|
+
...importTasks,
|
|
270
|
+
items: mergeImportTaskItems(current.importTasks.items, importTasks.items),
|
|
271
|
+
total: Math.max(numberValue(importTasks.total, 0), importTasks.items.length, current.importTasks.items.length)
|
|
272
|
+
},
|
|
226
273
|
customerContractAnalyses: normalized.customerContractAnalyses
|
|
227
274
|
}));
|
|
228
275
|
return true;
|
|
@@ -235,7 +282,7 @@ function App() {
|
|
|
235
282
|
finally {
|
|
236
283
|
gate.finish(requestId);
|
|
237
284
|
}
|
|
238
|
-
}, []);
|
|
285
|
+
}, [loadImportTasksFallback]);
|
|
239
286
|
const scheduleTaskStateRecovery = React.useCallback(() => {
|
|
240
287
|
if (taskRecoveryTimerRef.current != null)
|
|
241
288
|
return;
|
|
@@ -243,11 +290,16 @@ function App() {
|
|
|
243
290
|
void loadTaskState().then(refreshed => {
|
|
244
291
|
if (!refreshed || taskRecoveryTimerRef.current == null)
|
|
245
292
|
return;
|
|
246
|
-
|
|
247
|
-
|
|
293
|
+
if (!hasLocalUploadPlaceholders(importTaskItemsRef.current)) {
|
|
294
|
+
window.clearInterval(taskRecoveryTimerRef.current);
|
|
295
|
+
taskRecoveryTimerRef.current = null;
|
|
296
|
+
}
|
|
248
297
|
});
|
|
249
298
|
}, 2000);
|
|
250
299
|
}, [loadTaskState]);
|
|
300
|
+
React.useEffect(() => {
|
|
301
|
+
importTaskItemsRef.current = data.importTasks.items;
|
|
302
|
+
}, [data.importTasks.items]);
|
|
251
303
|
React.useEffect(() => () => {
|
|
252
304
|
if (taskRecoveryTimerRef.current != null)
|
|
253
305
|
window.clearInterval(taskRecoveryTimerRef.current);
|
|
@@ -270,7 +322,7 @@ function App() {
|
|
|
270
322
|
contextRef.current = nextContext;
|
|
271
323
|
const normalized = normalizeWorkbenchData(nextContext.payload);
|
|
272
324
|
bridge.configure({ uploadPolicy: normalized.uploadPolicy });
|
|
273
|
-
setData(normalized);
|
|
325
|
+
setData(current => mergeWorkbenchDataWithLocalTasks(current, normalized));
|
|
274
326
|
void load(nextContext);
|
|
275
327
|
},
|
|
276
328
|
onHostEvent() {
|
|
@@ -385,6 +437,7 @@ function App() {
|
|
|
385
437
|
signal,
|
|
386
438
|
pollTask: async (taskId) => asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
|
|
387
439
|
onTransition: (previous, next) => {
|
|
440
|
+
upsertLocalImportTask(next);
|
|
388
441
|
const notice = taskTransition(previous, next);
|
|
389
442
|
if (notice)
|
|
390
443
|
setToast(notice);
|
|
@@ -394,6 +447,7 @@ function App() {
|
|
|
394
447
|
if (completion.task.status === 'AWAITING_REVIEW') {
|
|
395
448
|
enqueuePendingReview({ kind: 'CONTRACT_IMPORT', completion });
|
|
396
449
|
}
|
|
450
|
+
upsertLocalImportTask(completion.task);
|
|
397
451
|
return completion;
|
|
398
452
|
}
|
|
399
453
|
catch (error) {
|
|
@@ -415,22 +469,85 @@ function App() {
|
|
|
415
469
|
setToast({ message: error instanceof Error ? error.message : '合同解析失败', tone: 'error' });
|
|
416
470
|
throw error;
|
|
417
471
|
}
|
|
418
|
-
}, [enqueuePendingReview, scheduleTaskStateRecovery]);
|
|
472
|
+
}, [enqueuePendingReview, scheduleTaskStateRecovery, upsertLocalImportTask]);
|
|
473
|
+
const startTrackingUploadedImportTask = React.useCallback((task) => {
|
|
474
|
+
if (task.documentType === 'PURCHASE_CONTRACT' || task.documentType === 'CUSTOMER_CONTRACT') {
|
|
475
|
+
const running = contractTaskTrackerRef.current.run(task.id, signal => orchestrateContractImport({ success: true, data: { task: task } }, signal));
|
|
476
|
+
if (running)
|
|
477
|
+
void running.catch(handleBackgroundImportTrackingError);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (task.documentType === 'CONTROLLED_CATALOG' || task.documentType === 'SANCTION_CATALOG') {
|
|
481
|
+
const running = catalogTaskTrackerRef.current.run(task.id, signal => orchestrateImport({ success: true, data: { task: task } }, signal));
|
|
482
|
+
if (running)
|
|
483
|
+
void running.catch(handleBackgroundImportTrackingError);
|
|
484
|
+
}
|
|
485
|
+
}, [handleBackgroundImportTrackingError, orchestrateContractImport, orchestrateImport]);
|
|
486
|
+
const presentImmediateContractUploadReview = React.useCallback(async (actionKey, start) => {
|
|
487
|
+
const data = asRecord(start.data);
|
|
488
|
+
const task = importTaskFromStart(start, actionKey);
|
|
489
|
+
if (actionKey === 'upload_customer_contract' && data.review) {
|
|
490
|
+
blobUrlOwnerRef.current.clear('customer');
|
|
491
|
+
const reviewTask = task ? { ...task, status: 'AWAITING_REVIEW' } : null;
|
|
492
|
+
if (reviewTask?.id) {
|
|
493
|
+
activeReviewResultRef.current = reviewTask.id;
|
|
494
|
+
handledContractTasksRef.current.add(reviewTask.id);
|
|
495
|
+
upsertLocalImportTask(reviewTask);
|
|
496
|
+
}
|
|
497
|
+
const review = adaptCustomerContractReview(data.review, {
|
|
498
|
+
reviewSource: 'IMPORT_DRAFT',
|
|
499
|
+
importTaskId: reviewTask?.id,
|
|
500
|
+
sourceFileId: reviewTask?.sourceFileId ?? '',
|
|
501
|
+
sourceFileName: reviewTask?.sourceFileName ?? ''
|
|
502
|
+
});
|
|
503
|
+
setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: reviewTask?.id ?? '', review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
|
|
504
|
+
changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
if (actionKey === 'upload_purchase_contract' && task?.documentType === 'PURCHASE_CONTRACT' && task.resultPayload) {
|
|
508
|
+
blobUrlOwnerRef.current.clear('purchase');
|
|
509
|
+
const reviewTask = { ...task, status: 'AWAITING_REVIEW' };
|
|
510
|
+
activeReviewResultRef.current = reviewTask.id;
|
|
511
|
+
handledContractTasksRef.current.add(reviewTask.id);
|
|
512
|
+
upsertLocalImportTask(reviewTask);
|
|
513
|
+
const draft = adaptPurchaseReview(reviewTask);
|
|
514
|
+
setPurchaseImportTask(reviewTask);
|
|
515
|
+
setPurchaseReviewDraft(await attachPurchaseSource(draft, reviewTask, blobUrlOwnerRef.current));
|
|
516
|
+
changeRouteRef.current({ type: 'procurement', level: 'suppliers' });
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}, [upsertLocalImportTask]);
|
|
419
521
|
const executeFileAction = React.useCallback(async (actionKey, file, input, notifyError = true) => {
|
|
420
522
|
try {
|
|
421
|
-
const raw = await
|
|
523
|
+
const raw = await bridge.executeFileAction(actionKey, file, input);
|
|
422
524
|
const start = asActionResult(unwrapResponse(raw));
|
|
525
|
+
const task = importTaskFromStart(start, actionKey, input, file);
|
|
526
|
+
if (task) {
|
|
527
|
+
upsertLocalImportTask(task);
|
|
528
|
+
if (isLocalUploadPlaceholder(task))
|
|
529
|
+
scheduleTaskStateRecovery();
|
|
530
|
+
else
|
|
531
|
+
startTrackingUploadedImportTask(task);
|
|
532
|
+
}
|
|
423
533
|
if (start.success === false)
|
|
424
534
|
throw actionResponseError(start, '上传失败');
|
|
535
|
+
await loadTaskState().catch((refreshError) => {
|
|
536
|
+
notifyTaskRecoveryRefreshError(refreshError instanceof Error ? refreshError : new Error(String(refreshError)));
|
|
537
|
+
});
|
|
538
|
+
await presentImmediateContractUploadReview(actionKey, start);
|
|
425
539
|
setToast({ message: resolveActionMessage(start) || '解析任务已提交', tone: 'info' });
|
|
426
540
|
return start;
|
|
427
541
|
}
|
|
428
542
|
catch (error) {
|
|
543
|
+
await loadTaskState().catch((refreshError) => {
|
|
544
|
+
notifyTaskRecoveryRefreshError(refreshError instanceof Error ? refreshError : new Error(String(refreshError)));
|
|
545
|
+
});
|
|
429
546
|
if (notifyError && !(error instanceof CatalogImportError))
|
|
430
547
|
setToast({ message: error instanceof Error ? error.message : '上传失败', tone: 'error' });
|
|
431
548
|
throw error;
|
|
432
549
|
}
|
|
433
|
-
}, [loadTaskState]);
|
|
550
|
+
}, [loadTaskState, notifyTaskRecoveryRefreshError, presentImmediateContractUploadReview, scheduleTaskStateRecovery, startTrackingUploadedImportTask, upsertLocalImportTask]);
|
|
434
551
|
const executeCatalogFileAction = React.useCallback((actionKey, file, input) => executeFileAction(actionKey, file, input, false), [executeFileAction]);
|
|
435
552
|
React.useEffect(() => {
|
|
436
553
|
const tasks = data.importTasks.items;
|
|
@@ -441,9 +558,9 @@ function App() {
|
|
|
441
558
|
for (const active of findActiveCatalogImportTasks(tasks)) {
|
|
442
559
|
const running = catalogTaskTrackerRef.current.run(active.id, signal => orchestrateImport({ success: true, data: { task: active } }, signal));
|
|
443
560
|
if (running)
|
|
444
|
-
void running.catch(
|
|
561
|
+
void running.catch(handleBackgroundImportTrackingError);
|
|
445
562
|
}
|
|
446
|
-
}, [data.importTasks.items, orchestrateImport]);
|
|
563
|
+
}, [data.importTasks.items, handleBackgroundImportTrackingError, orchestrateImport]);
|
|
447
564
|
React.useEffect(() => {
|
|
448
565
|
for (const task of data.importTasks.items) {
|
|
449
566
|
const conversationId = task.observerConversationId;
|
|
@@ -462,7 +579,7 @@ function App() {
|
|
|
462
579
|
continue;
|
|
463
580
|
const running = contractTaskTrackerRef.current.run(resumable.id, signal => orchestrateContractImport({ success: true, data: { task: resumable } }, signal));
|
|
464
581
|
if (running)
|
|
465
|
-
void running.catch(
|
|
582
|
+
void running.catch(handleBackgroundImportTrackingError);
|
|
466
583
|
}
|
|
467
584
|
for (const documentType of ['PURCHASE_CONTRACT', 'CUSTOMER_CONTRACT']) {
|
|
468
585
|
const failed = findRecoverableContractImport(tasks, documentType);
|
|
@@ -473,7 +590,7 @@ function App() {
|
|
|
473
590
|
else
|
|
474
591
|
setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: failed.errorMessage || '客户合同解析失败,请重新解析' });
|
|
475
592
|
}
|
|
476
|
-
if (!hasResumable && !failed && tasks.some(item => item.documentType === documentType && item.status === 'SUCCEEDED')) {
|
|
593
|
+
if (!activeReviewResultRef.current && !hasResumable && !failed && tasks.some(item => item.documentType === documentType && item.status === 'SUCCEEDED')) {
|
|
477
594
|
if (documentType === 'PURCHASE_CONTRACT') {
|
|
478
595
|
setPurchaseImportTask(null);
|
|
479
596
|
setPurchaseReviewDraft(null);
|
|
@@ -482,10 +599,10 @@ function App() {
|
|
|
482
599
|
setCustomerImportState({ status: 'IDLE' });
|
|
483
600
|
}
|
|
484
601
|
}
|
|
485
|
-
}, [data.importTasks.items, orchestrateContractImport]);
|
|
602
|
+
}, [data.importTasks.items, handleBackgroundImportTrackingError, orchestrateContractImport]);
|
|
486
603
|
const retryImport = React.useCallback(async (taskId) => {
|
|
487
604
|
try {
|
|
488
|
-
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
|
|
605
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState, notifyTaskRecoveryRefreshError);
|
|
489
606
|
const start = asActionResult(unwrapResponse(raw));
|
|
490
607
|
if (start.success === false)
|
|
491
608
|
throw actionResponseError(start, '重新解析失败');
|
|
@@ -496,10 +613,10 @@ function App() {
|
|
|
496
613
|
setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
|
|
497
614
|
throw error;
|
|
498
615
|
}
|
|
499
|
-
}, [loadTaskState]);
|
|
616
|
+
}, [loadTaskState, notifyTaskRecoveryRefreshError]);
|
|
500
617
|
const retryContractImport = React.useCallback(async (taskId) => {
|
|
501
618
|
try {
|
|
502
|
-
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
|
|
619
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState, notifyTaskRecoveryRefreshError);
|
|
503
620
|
const start = asActionResult(unwrapResponse(raw));
|
|
504
621
|
if (start.success === false)
|
|
505
622
|
throw actionResponseError(start, '重新解析失败');
|
|
@@ -509,7 +626,7 @@ function App() {
|
|
|
509
626
|
setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
|
|
510
627
|
throw error;
|
|
511
628
|
}
|
|
512
|
-
}, [loadTaskState]);
|
|
629
|
+
}, [loadTaskState, notifyTaskRecoveryRefreshError]);
|
|
513
630
|
const openImportTaskFromStatus = React.useCallback(async (task) => {
|
|
514
631
|
if (task.status === 'FAILED' || task.status === 'TIMED_OUT') {
|
|
515
632
|
if (task.documentType === 'PURCHASE_CONTRACT' || task.documentType === 'CUSTOMER_CONTRACT')
|
|
@@ -565,7 +682,7 @@ function App() {
|
|
|
565
682
|
const reanalyzeCustomerContract = React.useCallback(async (input) => {
|
|
566
683
|
setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
|
|
567
684
|
try {
|
|
568
|
-
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('reanalyze_customer_contract', { reviewSource: input.reviewSource, ...(input.customerContractId ? { id: input.customerContractId } : {}), ...(input.importTaskId ? { importTaskId: input.importTaskId } : {}) }), loadTaskState);
|
|
685
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('reanalyze_customer_contract', { reviewSource: input.reviewSource, ...(input.customerContractId ? { id: input.customerContractId } : {}), ...(input.importTaskId ? { importTaskId: input.importTaskId } : {}) }), loadTaskState, notifyTaskRecoveryRefreshError);
|
|
569
686
|
const response = asActionResult(unwrapResponse(raw));
|
|
570
687
|
if (response.success === false || !response.data)
|
|
571
688
|
throw actionResponseError(response, 'AI 重新分析启动失败');
|
|
@@ -727,7 +844,22 @@ function App() {
|
|
|
727
844
|
const requestId = gate.begin();
|
|
728
845
|
setBusy(true);
|
|
729
846
|
try {
|
|
730
|
-
const
|
|
847
|
+
const requestCatalogPage = async (page) => asRecord(unwrapResponse(await bridge.requestData({
|
|
848
|
+
page,
|
|
849
|
+
pageSize: requestedPageSize,
|
|
850
|
+
parameters: { collection, filters: encodeViewFilters(query.filters), refreshNonce: nextSummaryRequestNonce() }
|
|
851
|
+
})));
|
|
852
|
+
let committedPage = query.page;
|
|
853
|
+
let payload = await requestCatalogPage(committedPage);
|
|
854
|
+
let total = numberValue(payload.total, 0);
|
|
855
|
+
const maxPage = Math.max(1, Math.ceil(total / requestedPageSize));
|
|
856
|
+
if (query.page > maxPage) {
|
|
857
|
+
if (!gate.isLatest(requestId))
|
|
858
|
+
return;
|
|
859
|
+
committedPage = maxPage;
|
|
860
|
+
payload = await requestCatalogPage(maxPage);
|
|
861
|
+
total = numberValue(payload.total, 0);
|
|
862
|
+
}
|
|
731
863
|
if (!gate.isLatest(requestId))
|
|
732
864
|
return;
|
|
733
865
|
setData((current) => ({
|
|
@@ -735,8 +867,8 @@ function App() {
|
|
|
735
867
|
[collection]: {
|
|
736
868
|
...current[collection],
|
|
737
869
|
items: Array.isArray(payload.items) ? payload.items : [],
|
|
738
|
-
total
|
|
739
|
-
page: numberValue(payload.page,
|
|
870
|
+
total,
|
|
871
|
+
page: numberValue(payload.page, committedPage),
|
|
740
872
|
pageSize: requestedPageSize
|
|
741
873
|
}
|
|
742
874
|
}));
|
|
@@ -773,7 +905,7 @@ function App() {
|
|
|
773
905
|
}, []);
|
|
774
906
|
const queryProcurement = React.useCallback(async (_collection, query) => {
|
|
775
907
|
await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
|
|
776
|
-
}, [loadRoute]);
|
|
908
|
+
}, [loadImportTasksFallback, loadRoute]);
|
|
777
909
|
const queryCustomerContracts = React.useCallback(async (query) => {
|
|
778
910
|
await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
|
|
779
911
|
}, [loadRoute]);
|
|
@@ -824,7 +956,7 @@ function App() {
|
|
|
824
956
|
requests.finish(requestId);
|
|
825
957
|
setBusy(requests.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() > 0);
|
|
826
958
|
}
|
|
827
|
-
}, []);
|
|
959
|
+
}, [loadImportTasksFallback]);
|
|
828
960
|
const exportAnalytics = React.useCallback(async (_request) => {
|
|
829
961
|
setActionBusy(true);
|
|
830
962
|
try {
|
|
@@ -907,14 +1039,14 @@ function App() {
|
|
|
907
1039
|
const taskStatusItems = React.useMemo(() => buildTaskStatusItems(data.importTasks.items, openImportTaskFromStatus), [data.importTasks.items, openImportTaskFromStatus]);
|
|
908
1040
|
const contractCommands = page === 'procurement' && route.type === 'procurement'
|
|
909
1041
|
? route.level === 'suppliers'
|
|
910
|
-
? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u91C7\u8D2D\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_purchase_contract', file, { documentType: 'PURCHASE_CONTRACT' }) })
|
|
1042
|
+
? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u91C7\u8D2D\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_purchase_contract', file, { documentType: 'PURCHASE_CONTRACT' }), onError: message => setToast({ message, tone: 'error' }) })
|
|
911
1043
|
: undefined
|
|
912
1044
|
: page === 'customer-contract-review'
|
|
913
|
-
? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u5BA2\u6237\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_customer_contract', file, { documentType: 'CUSTOMER_CONTRACT' }) })
|
|
1045
|
+
? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u5BA2\u6237\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_customer_contract', file, { documentType: 'CUSTOMER_CONTRACT' }), onError: message => setToast({ message, tone: 'error' }) })
|
|
914
1046
|
: undefined;
|
|
915
1047
|
if (analyticsPrototypeVisible)
|
|
916
|
-
return (_jsx(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, topbarActions: _jsx(TaskStatusMenu, { items: taskStatusItems, busy: busy || actionBusy }), onRouteChange: changeRoute, onRefresh: () => void load(), children: _jsx(AnalyticsPrototypeBoard, { onClose: () => setAnalyticsPrototypeVisible(false) }) }));
|
|
917
|
-
return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: page === 'home' ? undefined : meta.subtitle, topbarActions: _jsx(TaskStatusMenu, { items: taskStatusItems, busy: busy || actionBusy }), commands: (page === 'controlled-goods' || page === 'sanctioned-companies') ? _jsx(CatalogCommands, { busy: busy || actionBusy, onUpload: () => setCatalogCommand('upload'), onCreate: () => setCatalogCommand('create') }) : contractCommands, busy: busy || actionBusy, onRouteChange: changeRoute, onRefresh: () => void load(), children: [page === 'home' && _jsx(HomePage, {}), page === 'controlled-goods' && _jsx(ControlledGoodsPage, { collection: data.controlledGoods, batches: data.controlledBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeCatalogFileAction, onRefresh: load, onQuery: (query) => queryCatalog('controlledGoods', query), onIssueQuery: queryImportIssues, importCompletion: importCompletions.CONTROLLED_CATALOG, importFailure: importFailures.CONTROLLED_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null), onNotify: setToast }), page === 'sanctioned-companies' && _jsx(SanctionedCompaniesPage, { collection: data.sanctionedCompanies, batches: data.sanctionBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeCatalogFileAction, onRefresh: load, onQuery: (query) => queryCatalog('sanctionedCompanies', query), onIssueQuery: queryImportIssues, importCompletion: importCompletions.SANCTION_CATALOG, importFailure: importFailures.SANCTION_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null), onNotify: setToast }), page === 'procurement' && route.type === 'procurement' && _jsx(ProcurementPage, { route: route, suppliers: data.suppliers, purchaseOrders: data.purchaseOrders, purchaseLines: data.purchaseLines, supplier: procurementContext.supplier, order: procurementContext.order, metrics: procurementContext.metrics, importTask: purchaseImportTask, reviewDraft: purchaseReviewDraft, busy: busy || actionBusy, onQuery: queryProcurement, onAction: executeAction, onFileAction: executeFileAction, onRouteChange: changeRoute, onRetryImport: retryContractImport, onReviewClose: closePurchaseReview, onNotify: setToast }), page === 'customer-contract-review' && _jsx(CustomerContractsPage, { collection: data.customerContracts, busy: busy || actionBusy, importReviewState: customerImportState, onQuery: queryCustomerContracts, onAction: executeAction, onFileAction: executeFileAction, onReanalyze: reanalyzeCustomerContract, onLoadReview: loadCustomerContractReview, onReviewClose: closeCustomerReview, reanalysisState: customerReanalysisState, onRetryImport: retryContractImport, onNotify: setToast }), page === 'sales' && route.type === 'sales' && _jsx(SalesPage, { route: route, customers: data.salesCustomers, orders: data.salesOrders, lines: data.salesLines, busy: busy || actionBusy, onQuery: querySales, onAction: executeAction, onRouteChange: changeRoute, onOpenFiles: openSalesFiles, onNotify: setToast }), page === 'sales-files' && _jsx(SalesFilesPage, { orders: data.salesFileOrders, history: data.generatedFiles, activeOrder: activeSalesFileOrder, visualPrototypeState: salesFilesPrototypeState, banks: data.settings.banks, busy: busy || actionBusy, onQuery: querySalesFiles, onAction: executeSalesFileAction, onOpenSalesOrder: selectSalesFileOrder, onDownload: file => executeAction('download_sales_file', { id: file.id }).then(() => undefined), onEdit: async (file) => setEditingSalesFile(file), onNotify: setToast }), page === 'analytics' && _jsx(AnalyticsPage, { data: data.analytics, committedQuery: analyticsCommittedQuery, busy: busy || actionBusy, error: analyticsError, onQuery: queryAnalytics, onExportXlsx: exportAnalytics }), page === 'settings' && _jsx(SettingsPage, { data: data.settings, busy: busy || actionBusy, onSave: saveSettings, onRestoreDefault: restoreSettings, onDeleteRate: deleteSettingsRate, onRateQuery: queryMonthlyRates, onUploadLogo: uploadCompanyLogo, onDiscardLogo: discardCompanyLogo, onNotify: setToast }), !['home', 'controlled-goods', 'sanctioned-companies', 'procurement', 'customer-contract-review', 'sales', 'sales-files', 'analytics', 'settings'].includes(page) && _jsxs("section", { className: "tcw-page-placeholder", "data-page": page, children: [_jsx(InfoNotice, { children: "\u9875\u9762\u57FA\u7840\u7ED3\u6784\u5DF2\u5C31\u7EEA\uFF0C\u4E1A\u52A1\u5185\u5BB9\u5C06\u5728\u5BF9\u5E94\u9875\u9762\u6A21\u5757\u4E2D\u52A0\u8F7D\u3002" }), _jsx("div", { className: "tcw-placeholder-body", "aria-label": `${meta.title}页面内容` })] })] }), _jsx(LoadingBlocker, { visible: busy || actionBusy, label: actionBusy ? '操作处理中' : '数据加载中' }), _jsx(ToastStack, { items: toasts, onClose: closeToast }), editingSalesFile && _jsx(SalesFileEditor, { file: editingSalesFile, onNotify: setToast, onClose: () => setEditingSalesFile(null), onSaved: () => { setEditingSalesFile(null); if (activeSalesFileOrder)
|
|
1048
|
+
return (_jsx(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, topbarActions: _jsx(TaskStatusMenu, { items: taskStatusItems, busy: busy || actionBusy, onActionError: notifyTaskStatusActionError }), onRouteChange: changeRoute, onRefresh: () => void load(), children: _jsx(AnalyticsPrototypeBoard, { onClose: () => setAnalyticsPrototypeVisible(false) }) }));
|
|
1049
|
+
return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: page === 'home' ? undefined : meta.subtitle, topbarActions: _jsx(TaskStatusMenu, { items: taskStatusItems, busy: busy || actionBusy, onActionError: notifyTaskStatusActionError }), commands: (page === 'controlled-goods' || page === 'sanctioned-companies') ? _jsx(CatalogCommands, { busy: busy || actionBusy, onUpload: () => setCatalogCommand('upload'), onCreate: () => setCatalogCommand('create') }) : contractCommands, busy: busy || actionBusy, onRouteChange: changeRoute, onRefresh: () => void load(), children: [page === 'home' && _jsx(HomePage, {}), page === 'controlled-goods' && _jsx(ControlledGoodsPage, { collection: data.controlledGoods, batches: data.controlledBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeCatalogFileAction, onRefresh: load, onQuery: (query) => queryCatalog('controlledGoods', query), onIssueQuery: queryImportIssues, importCompletion: importCompletions.CONTROLLED_CATALOG, importFailure: importFailures.CONTROLLED_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null), onNotify: setToast }), page === 'sanctioned-companies' && _jsx(SanctionedCompaniesPage, { collection: data.sanctionedCompanies, batches: data.sanctionBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeCatalogFileAction, onRefresh: load, onQuery: (query) => queryCatalog('sanctionedCompanies', query), onIssueQuery: queryImportIssues, importCompletion: importCompletions.SANCTION_CATALOG, importFailure: importFailures.SANCTION_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null), onNotify: setToast }), page === 'procurement' && route.type === 'procurement' && _jsx(ProcurementPage, { route: route, suppliers: data.suppliers, purchaseOrders: data.purchaseOrders, purchaseLines: data.purchaseLines, supplier: procurementContext.supplier, order: procurementContext.order, metrics: procurementContext.metrics, importTask: purchaseImportTask, reviewDraft: purchaseReviewDraft, busy: busy || actionBusy, onQuery: queryProcurement, onAction: executeAction, onFileAction: executeFileAction, onRouteChange: changeRoute, onRetryImport: retryContractImport, onReviewClose: closePurchaseReview, onNotify: setToast }), page === 'customer-contract-review' && _jsx(CustomerContractsPage, { collection: data.customerContracts, busy: busy || actionBusy, importReviewState: customerImportState, onQuery: queryCustomerContracts, onAction: executeAction, onFileAction: executeFileAction, onReanalyze: reanalyzeCustomerContract, onLoadReview: loadCustomerContractReview, onReviewClose: closeCustomerReview, reanalysisState: customerReanalysisState, onRetryImport: retryContractImport, onNotify: setToast }), page === 'sales' && route.type === 'sales' && _jsx(SalesPage, { route: route, customers: data.salesCustomers, orders: data.salesOrders, lines: data.salesLines, busy: busy || actionBusy, onQuery: querySales, onAction: executeAction, onRouteChange: changeRoute, onOpenFiles: openSalesFiles, onNotify: setToast }), page === 'sales-files' && _jsx(SalesFilesPage, { orders: data.salesFileOrders, history: data.generatedFiles, activeOrder: activeSalesFileOrder, visualPrototypeState: salesFilesPrototypeState, onPrototypeStateClose: () => setSalesFilesPrototypeState(undefined), banks: data.settings.banks, busy: busy || actionBusy, onQuery: querySalesFiles, onAction: executeSalesFileAction, onOpenSalesOrder: selectSalesFileOrder, onDownload: file => executeAction('download_sales_file', { id: file.id }).then(() => undefined), onEdit: async (file) => setEditingSalesFile(file), onNotify: setToast }), page === 'analytics' && _jsx(AnalyticsPage, { data: data.analytics, committedQuery: analyticsCommittedQuery, busy: busy || actionBusy, error: analyticsError, onQuery: queryAnalytics, onExportXlsx: exportAnalytics }), page === 'settings' && _jsx(SettingsPage, { data: data.settings, busy: busy || actionBusy, onSave: saveSettings, onRestoreDefault: restoreSettings, onDeleteRate: deleteSettingsRate, onRateQuery: queryMonthlyRates, onUploadLogo: uploadCompanyLogo, onDiscardLogo: discardCompanyLogo, onNotify: setToast }), !['home', 'controlled-goods', 'sanctioned-companies', 'procurement', 'customer-contract-review', 'sales', 'sales-files', 'analytics', 'settings'].includes(page) && _jsxs("section", { className: "tcw-page-placeholder", "data-page": page, children: [_jsx(InfoNotice, { children: "\u9875\u9762\u57FA\u7840\u7ED3\u6784\u5DF2\u5C31\u7EEA\uFF0C\u4E1A\u52A1\u5185\u5BB9\u5C06\u5728\u5BF9\u5E94\u9875\u9762\u6A21\u5757\u4E2D\u52A0\u8F7D\u3002" }), _jsx("div", { className: "tcw-placeholder-body", "aria-label": `${meta.title}页面内容` })] })] }), _jsx(LoadingBlocker, { visible: busy || actionBusy, label: actionBusy ? '操作处理中' : '数据加载中' }), _jsx(ToastStack, { items: toasts, onClose: closeToast }), editingSalesFile && _jsx(SalesFileEditor, { file: editingSalesFile, onNotify: setToast, onClose: () => setEditingSalesFile(null), onSaved: () => { setEditingSalesFile(null); if (activeSalesFileOrder)
|
|
918
1050
|
void querySalesFiles('generatedFiles', { page: 1, pageSize: 20, filters: {}, parameters: { customerContractId: activeSalesFileOrder.customerContractId, purchaseOrderId: activeSalesFileOrder.purchaseOrderId } }); } })] }));
|
|
919
1051
|
}
|
|
920
1052
|
function routePage(route) {
|
|
@@ -937,7 +1069,7 @@ function buildTaskStatusItems(tasks, onOpen) {
|
|
|
937
1069
|
statusLabel: taskStatusLabel(task.status),
|
|
938
1070
|
tone,
|
|
939
1071
|
updatedAt: formatTaskTime(task.updatedAt ?? task.queuedAt ?? task.createdAt),
|
|
940
|
-
progressPercent:
|
|
1072
|
+
progressPercent: importTaskProgressPercent(task),
|
|
941
1073
|
...((task.status === 'AWAITING_REVIEW' || task.status === 'FAILED' || task.status === 'TIMED_OUT') ? { actionLabel: task.status === 'AWAITING_REVIEW' ? '审核' : '重试', onAction: () => onOpen(task) } : {})
|
|
942
1074
|
};
|
|
943
1075
|
});
|
|
@@ -965,21 +1097,6 @@ function documentTypeLabel(documentType) {
|
|
|
965
1097
|
};
|
|
966
1098
|
return labels[documentType] ?? documentType;
|
|
967
1099
|
}
|
|
968
|
-
function taskProgressPercent(task) {
|
|
969
|
-
if (task.status === 'SUCCEEDED')
|
|
970
|
-
return 100;
|
|
971
|
-
if (task.status === 'AWAITING_REVIEW')
|
|
972
|
-
return 92;
|
|
973
|
-
if (task.status === 'FAILED' || task.status === 'TIMED_OUT')
|
|
974
|
-
return null;
|
|
975
|
-
const progress = 'progress' in task ? task.progress : null;
|
|
976
|
-
const processed = Number(progress?.processedCount);
|
|
977
|
-
const total = Number(progress?.successCount) + Number(progress?.duplicateCount) + Number(progress?.invalidCount) + Number(progress?.reviewPendingCount);
|
|
978
|
-
if (Number.isFinite(processed) && Number.isFinite(total) && total > 0)
|
|
979
|
-
return Math.min(90, Math.max(5, (processed / total) * 90));
|
|
980
|
-
const fallback = { QUEUED: 8, EXTRACTING: 24, AI_PARSING: 54, VALIDATING: 74, PERSISTING: 88 };
|
|
981
|
-
return fallback[task.status] ?? null;
|
|
982
|
-
}
|
|
983
1100
|
function formatTaskTime(value) {
|
|
984
1101
|
if (typeof value !== 'string' || !value)
|
|
985
1102
|
return null;
|
|
@@ -988,6 +1105,25 @@ function formatTaskTime(value) {
|
|
|
988
1105
|
return value;
|
|
989
1106
|
return new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }).format(date).replace(/\//g, '/');
|
|
990
1107
|
}
|
|
1108
|
+
function mergeWorkbenchDataWithLocalTasks(current, incoming) {
|
|
1109
|
+
const items = mergeImportTaskItems(current.importTasks.items, incoming.importTasks.items);
|
|
1110
|
+
return {
|
|
1111
|
+
...incoming,
|
|
1112
|
+
importTasks: {
|
|
1113
|
+
...incoming.importTasks,
|
|
1114
|
+
items,
|
|
1115
|
+
total: Math.max(numberValue(incoming.importTasks.total, 0), incoming.importTasks.items.length, current.importTasks.items.length, items.length),
|
|
1116
|
+
page: numberValue(incoming.importTasks.page, numberValue(current.importTasks.page, 1)),
|
|
1117
|
+
pageSize: incoming.importTasks.pageSize ?? current.importTasks.pageSize ?? 100
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function hasLocalUploadPlaceholders(items) {
|
|
1122
|
+
return items.some(item => {
|
|
1123
|
+
const task = item && typeof item === 'object' && !Array.isArray(item) ? item : null;
|
|
1124
|
+
return task ? isLocalUploadPlaceholder(task) : false;
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
991
1127
|
function analyticsQueryFromRoute(page, pageSize, values) {
|
|
992
1128
|
const requestedPageSize = resolvePaginationPageSize(pageSize);
|
|
993
1129
|
const dateValue = values.orderDate;
|
|
@@ -1028,6 +1164,26 @@ function actionResponseError(result, fallback) {
|
|
|
1028
1164
|
function asRecord(value) {
|
|
1029
1165
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
1030
1166
|
}
|
|
1167
|
+
function summaryRequestParameters(value) {
|
|
1168
|
+
const parameters = { ...asRecord(value) };
|
|
1169
|
+
delete parameters.collection;
|
|
1170
|
+
delete parameters.filters;
|
|
1171
|
+
delete parameters.taskId;
|
|
1172
|
+
delete parameters.runId;
|
|
1173
|
+
delete parameters.customerContractId;
|
|
1174
|
+
delete parameters.purchaseOrderId;
|
|
1175
|
+
return parameters;
|
|
1176
|
+
}
|
|
1177
|
+
function summaryRequestQuery(parameters) {
|
|
1178
|
+
return { page: 1, pageSize: 100, search: nextSummaryRequestNonce(), parameters: summaryRequestParameters(parameters) };
|
|
1179
|
+
}
|
|
1180
|
+
function importTasksCollectionRequest() {
|
|
1181
|
+
return { page: 1, pageSize: 100, parameters: { collection: 'importTasks', filters: '{}', refreshNonce: nextSummaryRequestNonce() } };
|
|
1182
|
+
}
|
|
1183
|
+
function nextSummaryRequestNonce() {
|
|
1184
|
+
summaryRequestSequence += 1;
|
|
1185
|
+
return `__tcw_summary_refresh__:${Date.now()}:${summaryRequestSequence}`;
|
|
1186
|
+
}
|
|
1031
1187
|
function numberValue(value, fallback) {
|
|
1032
1188
|
const result = Number(value);
|
|
1033
1189
|
return Number.isFinite(result) ? result : fallback;
|
|
@@ -1049,26 +1205,11 @@ function downloadActionFile(value) {
|
|
|
1049
1205
|
anchor.href = url;
|
|
1050
1206
|
anchor.download = file.fileName;
|
|
1051
1207
|
anchor.click();
|
|
1052
|
-
setTimeout(() => URL.revokeObjectURL(url),
|
|
1208
|
+
setTimeout(() => URL.revokeObjectURL(url), DOWNLOAD_URL_REVOKE_DELAY_MS);
|
|
1053
1209
|
}
|
|
1054
|
-
function ContractUploadCommand({ label, busy, onFile }) {
|
|
1210
|
+
function ContractUploadCommand({ label, busy, onFile, onError }) {
|
|
1055
1211
|
return _jsxs("label", { className: "tcw-button tcw-button--primary", children: [_jsx("i", { className: "ri-upload-cloud-2-line", "aria-hidden": "true" }), label, _jsx("input", { className: "tcw-procurement-file-input", type: "file", accept: ".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp", disabled: busy, onChange: event => { const file = event.target.files?.[0]; event.currentTarget.value = ''; if (file)
|
|
1056
|
-
void onFile(file).catch(
|
|
1057
|
-
}
|
|
1058
|
-
function adaptPurchaseReview(task) {
|
|
1059
|
-
const payload = asRecord(task.resultPayload);
|
|
1060
|
-
const supplier = asRecord(payload.supplier);
|
|
1061
|
-
const order = asRecord(payload.order);
|
|
1062
|
-
const lines = Array.isArray(payload.lines) ? payload.lines.map(value => {
|
|
1063
|
-
const line = asRecord(value);
|
|
1064
|
-
return { ...line, sourceType: line.sourceType === 'MANUAL_ENTRY' ? 'MANUAL_ENTRY' : 'CONTRACT_EXTRACTED', hsCodeStatus: line.hsCodeStatus === 'AI_CONFIRMED' || line.hsCodeStatus === 'MANUALLY_CONFIRMED' ? line.hsCodeStatus : 'UNCONFIRMED' };
|
|
1065
|
-
}) : [];
|
|
1066
|
-
return {
|
|
1067
|
-
supplier: { ...supplier, name: String(supplier.name ?? '') },
|
|
1068
|
-
order: { ...order, supplierId: String(order.supplierId ?? ''), contractNo: String(order.contractNo ?? ''), orderDate: String(order.orderDate ?? ''), contractAmountCny: String(order.contractAmountCny ?? '0') },
|
|
1069
|
-
lines,
|
|
1070
|
-
sourceFileName: task.sourceFileName ?? null
|
|
1071
|
-
};
|
|
1212
|
+
void onFile(file).catch(error => onError?.(error instanceof Error ? error.message : '合同上传失败')); } })] });
|
|
1072
1213
|
}
|
|
1073
1214
|
async function attachPurchaseSource(draft, task, owner) {
|
|
1074
1215
|
if (draft.sourceFileUrl || !task.sourceFileId)
|
|
@@ -1107,7 +1248,7 @@ async function loadSourcePreview(sourceFileId, slot, owner) {
|
|
|
1107
1248
|
throw new Error('源文件预览响应类型无效');
|
|
1108
1249
|
}
|
|
1109
1250
|
function textPreviewUrl(title, content) {
|
|
1110
|
-
const html = `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>${escapeHtml(title)}</title><style>body{margin:0;padding:
|
|
1251
|
+
const html = `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>${escapeHtml(title)}</title><style>body{margin:0;padding:16px;font:14px/1.7 system-ui,sans-serif;color:#1f2937;background:#fff}pre{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><pre>${escapeHtml(content)}</pre></body></html>`;
|
|
1111
1252
|
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
|
1112
1253
|
}
|
|
1113
1254
|
function escapeHtml(value) { return value.replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character] ?? character); }
|