@chenchaolong/plugin-trade-compliance-workbench 1.0.17 → 1.0.19
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/domain/contract-number.js +2 -2
- package/dist/lib/domain/contract-number.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 +271 -48
- 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/layout.d.ts +3 -2
- package/dist/lib/remote-ui/components/layout.d.ts.map +1 -1
- package/dist/lib/remote-ui/components/layout.js +4 -4
- package/dist/lib/remote-ui/components/layout.js.map +1 -1
- package/dist/lib/remote-ui/components/task-status.d.ts +20 -0
- package/dist/lib/remote-ui/components/task-status.d.ts.map +1 -0
- package/dist/lib/remote-ui/components/task-status.js +30 -0
- package/dist/lib/remote-ui/components/task-status.js.map +1 -0
- package/dist/lib/remote-ui/contract-import.d.ts.map +1 -1
- package/dist/lib/remote-ui/contract-import.js +6 -2
- package/dist/lib/remote-ui/contract-import.js.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 +5 -3
- package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.js +6 -6
- 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/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.map +1 -1
- package/dist/lib/workbench.service.js +36 -10
- 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"}
|
|
@@ -7,17 +7,20 @@ import { clampPageAfterSingleDeletion, createBlobUrlOwner, createKeyedLatestRequ
|
|
|
7
7
|
import { encodeViewFilters } from '../view-parameters.js';
|
|
8
8
|
import { InfoNotice } from './components/data-grid.js';
|
|
9
9
|
import { LoadingBlocker, ToastStack } from './components/forms.js';
|
|
10
|
+
import { TaskStatusMenu } from './components/task-status.js';
|
|
10
11
|
import { addToast, createToastId, createToastTimerRegistry, removeToast } from './toast-queue.js';
|
|
11
12
|
import { buildBreadcrumbs, WorkbenchShell } from './components/layout.js';
|
|
12
13
|
import { CatalogCommands, CatalogImportError, ControlledGoodsPage, findActiveCatalogImportTasks, findLatestFailedCatalogImportTask, runCatalogImportOrchestration, SanctionedCompaniesPage } from './pages/catalogs.js';
|
|
13
14
|
import { HomePage } from './pages/home.js';
|
|
14
15
|
import { ProcurementPage } from './pages/procurement.js';
|
|
16
|
+
import { adaptPurchaseReview } from './purchase-review-adapter.js';
|
|
15
17
|
import { CustomerContractsPage, adaptCustomerContractReview } from './pages/contracts.js';
|
|
16
18
|
import { SalesPage } from './pages/sales.js';
|
|
17
19
|
import { SalesFilesPage } from './pages/files.js';
|
|
18
20
|
import { AnalyticsPage, AnalyticsPrototypeBoard } from './pages/analytics.js';
|
|
19
21
|
import { SettingsPage } from './pages/settings.js';
|
|
20
22
|
import { ContractImportError, findRecoverableContractImport, findResumableContractImports, runContractImportOrchestration, runCustomerContractReanalysis, taskTransition } from './contract-import.js';
|
|
23
|
+
import { importTaskFromStart, importTaskProgressPercent, isLocalUploadPlaceholder, mergeImportTaskItems } from './import-task-status.js';
|
|
21
24
|
import { compareTaskBusinessOrder, createPendingResultQueue, createPollingTaskTracker, isAbortError, isPollingObservationTimeout, submitAndRefreshTaskState } from './polling.js';
|
|
22
25
|
import { ACTION_NOTIFICATION_POLICIES, executeWorkbenchAction } from './action-execution.js';
|
|
23
26
|
import './styles.css';
|
|
@@ -33,12 +36,14 @@ const pageMeta = {
|
|
|
33
36
|
settings: { title: '系统设置', subtitle: '维护企业、银行、汇率和利润规则。' }
|
|
34
37
|
};
|
|
35
38
|
const procurementPageMeta = {
|
|
36
|
-
suppliers: { title: '供应商列表', subtitle: '
|
|
39
|
+
suppliers: { title: '供应商列表', subtitle: '管理供应商主档,并通过供应商代码进入该供应商的采购订单列表。' },
|
|
37
40
|
orders: { title: '采购订单列表', subtitle: '查看当前供应商的采购订单、金额及合规状态。' },
|
|
38
41
|
lines: { title: '采购商品列表', subtitle: '查看当前采购订单的商品明细及合规状态。' }
|
|
39
42
|
};
|
|
40
43
|
const bridge = createRemoteBridge();
|
|
41
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;
|
|
42
47
|
function comparePendingReviewBusinessOrder(left, right) {
|
|
43
48
|
const leftTask = left.kind === 'CONTRACT_IMPORT' ? left.completion.task : left.result.run;
|
|
44
49
|
const rightTask = right.kind === 'CONTRACT_IMPORT' ? right.completion.task : right.result.run;
|
|
@@ -71,6 +76,7 @@ function App() {
|
|
|
71
76
|
const [analyticsPrototypeVisible, setAnalyticsPrototypeVisible] = React.useState(false);
|
|
72
77
|
const [salesFilesPrototypeState, setSalesFilesPrototypeState] = React.useState(undefined);
|
|
73
78
|
const [data, setData] = React.useState(() => normalizeWorkbenchData(null));
|
|
79
|
+
const importTaskItemsRef = React.useRef([]);
|
|
74
80
|
const [busy, setBusy] = React.useState(false);
|
|
75
81
|
const [actionBusy, setActionBusy] = React.useState(false);
|
|
76
82
|
const [catalogCommand, setCatalogCommand] = React.useState(null);
|
|
@@ -105,6 +111,42 @@ function App() {
|
|
|
105
111
|
toastTimers.dismiss(id);
|
|
106
112
|
setToasts((items) => removeToast(items, id));
|
|
107
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
|
+
}, []);
|
|
108
150
|
const configureAnalyticsPrototypeCapability = React.useCallback((parameters) => {
|
|
109
151
|
analyticsPrototypeListenerCleanupRef.current?.();
|
|
110
152
|
analyticsPrototypeListenerCleanupRef.current = null;
|
|
@@ -187,11 +229,12 @@ function App() {
|
|
|
187
229
|
const requestId = gate.begin();
|
|
188
230
|
setBusy(true);
|
|
189
231
|
try {
|
|
190
|
-
const response = await bridge.requestData(
|
|
232
|
+
const response = await bridge.requestData(summaryRequestQuery(active.initialQuery?.parameters));
|
|
191
233
|
if (gate.isLatest(requestId)) {
|
|
192
234
|
const normalized = normalizeWorkbenchData(response);
|
|
235
|
+
const importTasks = await loadImportTasksFallback(normalized);
|
|
193
236
|
bridge.configure({ uploadPolicy: normalized.uploadPolicy });
|
|
194
|
-
setData(normalized);
|
|
237
|
+
setData(current => mergeWorkbenchDataWithLocalTasks(current, { ...normalized, importTasks }));
|
|
195
238
|
await loadRoute(routeRef.current);
|
|
196
239
|
}
|
|
197
240
|
}
|
|
@@ -203,7 +246,7 @@ function App() {
|
|
|
203
246
|
gate.finish(requestId);
|
|
204
247
|
setBusy(gate.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0);
|
|
205
248
|
}
|
|
206
|
-
}, [loadRoute]);
|
|
249
|
+
}, [loadImportTasksFallback, loadRoute]);
|
|
207
250
|
const loadTaskState = React.useCallback(async () => {
|
|
208
251
|
const active = contextRef.current;
|
|
209
252
|
if (!active)
|
|
@@ -211,17 +254,22 @@ function App() {
|
|
|
211
254
|
const gate = taskStateRequestGateRef.current;
|
|
212
255
|
const requestId = gate.begin();
|
|
213
256
|
try {
|
|
214
|
-
const response = await bridge.requestData(
|
|
257
|
+
const response = await bridge.requestData(summaryRequestQuery(active.initialQuery?.parameters));
|
|
215
258
|
if (!gate.commit(requestId))
|
|
216
259
|
return false;
|
|
217
260
|
const normalized = normalizeWorkbenchData(response);
|
|
261
|
+
const importTasks = await loadImportTasksFallback(normalized);
|
|
218
262
|
setData(current => ({
|
|
219
263
|
...current,
|
|
220
264
|
controlledGoods: normalized.controlledGoods,
|
|
221
265
|
controlledBatches: normalized.controlledBatches,
|
|
222
266
|
sanctionedCompanies: normalized.sanctionedCompanies,
|
|
223
267
|
sanctionBatches: normalized.sanctionBatches,
|
|
224
|
-
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
|
+
},
|
|
225
273
|
customerContractAnalyses: normalized.customerContractAnalyses
|
|
226
274
|
}));
|
|
227
275
|
return true;
|
|
@@ -234,7 +282,7 @@ function App() {
|
|
|
234
282
|
finally {
|
|
235
283
|
gate.finish(requestId);
|
|
236
284
|
}
|
|
237
|
-
}, []);
|
|
285
|
+
}, [loadImportTasksFallback]);
|
|
238
286
|
const scheduleTaskStateRecovery = React.useCallback(() => {
|
|
239
287
|
if (taskRecoveryTimerRef.current != null)
|
|
240
288
|
return;
|
|
@@ -242,11 +290,16 @@ function App() {
|
|
|
242
290
|
void loadTaskState().then(refreshed => {
|
|
243
291
|
if (!refreshed || taskRecoveryTimerRef.current == null)
|
|
244
292
|
return;
|
|
245
|
-
|
|
246
|
-
|
|
293
|
+
if (!hasLocalUploadPlaceholders(importTaskItemsRef.current)) {
|
|
294
|
+
window.clearInterval(taskRecoveryTimerRef.current);
|
|
295
|
+
taskRecoveryTimerRef.current = null;
|
|
296
|
+
}
|
|
247
297
|
});
|
|
248
298
|
}, 2000);
|
|
249
299
|
}, [loadTaskState]);
|
|
300
|
+
React.useEffect(() => {
|
|
301
|
+
importTaskItemsRef.current = data.importTasks.items;
|
|
302
|
+
}, [data.importTasks.items]);
|
|
250
303
|
React.useEffect(() => () => {
|
|
251
304
|
if (taskRecoveryTimerRef.current != null)
|
|
252
305
|
window.clearInterval(taskRecoveryTimerRef.current);
|
|
@@ -269,7 +322,7 @@ function App() {
|
|
|
269
322
|
contextRef.current = nextContext;
|
|
270
323
|
const normalized = normalizeWorkbenchData(nextContext.payload);
|
|
271
324
|
bridge.configure({ uploadPolicy: normalized.uploadPolicy });
|
|
272
|
-
setData(normalized);
|
|
325
|
+
setData(current => mergeWorkbenchDataWithLocalTasks(current, normalized));
|
|
273
326
|
void load(nextContext);
|
|
274
327
|
},
|
|
275
328
|
onHostEvent() {
|
|
@@ -384,6 +437,7 @@ function App() {
|
|
|
384
437
|
signal,
|
|
385
438
|
pollTask: async (taskId) => asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
|
|
386
439
|
onTransition: (previous, next) => {
|
|
440
|
+
upsertLocalImportTask(next);
|
|
387
441
|
const notice = taskTransition(previous, next);
|
|
388
442
|
if (notice)
|
|
389
443
|
setToast(notice);
|
|
@@ -393,6 +447,7 @@ function App() {
|
|
|
393
447
|
if (completion.task.status === 'AWAITING_REVIEW') {
|
|
394
448
|
enqueuePendingReview({ kind: 'CONTRACT_IMPORT', completion });
|
|
395
449
|
}
|
|
450
|
+
upsertLocalImportTask(completion.task);
|
|
396
451
|
return completion;
|
|
397
452
|
}
|
|
398
453
|
catch (error) {
|
|
@@ -414,22 +469,85 @@ function App() {
|
|
|
414
469
|
setToast({ message: error instanceof Error ? error.message : '合同解析失败', tone: 'error' });
|
|
415
470
|
throw error;
|
|
416
471
|
}
|
|
417
|
-
}, [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]);
|
|
418
521
|
const executeFileAction = React.useCallback(async (actionKey, file, input, notifyError = true) => {
|
|
419
522
|
try {
|
|
420
|
-
const raw = await
|
|
523
|
+
const raw = await bridge.executeFileAction(actionKey, file, input);
|
|
421
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
|
+
}
|
|
422
533
|
if (start.success === false)
|
|
423
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);
|
|
424
539
|
setToast({ message: resolveActionMessage(start) || '解析任务已提交', tone: 'info' });
|
|
425
540
|
return start;
|
|
426
541
|
}
|
|
427
542
|
catch (error) {
|
|
543
|
+
await loadTaskState().catch((refreshError) => {
|
|
544
|
+
notifyTaskRecoveryRefreshError(refreshError instanceof Error ? refreshError : new Error(String(refreshError)));
|
|
545
|
+
});
|
|
428
546
|
if (notifyError && !(error instanceof CatalogImportError))
|
|
429
547
|
setToast({ message: error instanceof Error ? error.message : '上传失败', tone: 'error' });
|
|
430
548
|
throw error;
|
|
431
549
|
}
|
|
432
|
-
}, [loadTaskState]);
|
|
550
|
+
}, [loadTaskState, notifyTaskRecoveryRefreshError, presentImmediateContractUploadReview, scheduleTaskStateRecovery, startTrackingUploadedImportTask, upsertLocalImportTask]);
|
|
433
551
|
const executeCatalogFileAction = React.useCallback((actionKey, file, input) => executeFileAction(actionKey, file, input, false), [executeFileAction]);
|
|
434
552
|
React.useEffect(() => {
|
|
435
553
|
const tasks = data.importTasks.items;
|
|
@@ -440,9 +558,9 @@ function App() {
|
|
|
440
558
|
for (const active of findActiveCatalogImportTasks(tasks)) {
|
|
441
559
|
const running = catalogTaskTrackerRef.current.run(active.id, signal => orchestrateImport({ success: true, data: { task: active } }, signal));
|
|
442
560
|
if (running)
|
|
443
|
-
void running.catch(
|
|
561
|
+
void running.catch(handleBackgroundImportTrackingError);
|
|
444
562
|
}
|
|
445
|
-
}, [data.importTasks.items, orchestrateImport]);
|
|
563
|
+
}, [data.importTasks.items, handleBackgroundImportTrackingError, orchestrateImport]);
|
|
446
564
|
React.useEffect(() => {
|
|
447
565
|
for (const task of data.importTasks.items) {
|
|
448
566
|
const conversationId = task.observerConversationId;
|
|
@@ -461,7 +579,7 @@ function App() {
|
|
|
461
579
|
continue;
|
|
462
580
|
const running = contractTaskTrackerRef.current.run(resumable.id, signal => orchestrateContractImport({ success: true, data: { task: resumable } }, signal));
|
|
463
581
|
if (running)
|
|
464
|
-
void running.catch(
|
|
582
|
+
void running.catch(handleBackgroundImportTrackingError);
|
|
465
583
|
}
|
|
466
584
|
for (const documentType of ['PURCHASE_CONTRACT', 'CUSTOMER_CONTRACT']) {
|
|
467
585
|
const failed = findRecoverableContractImport(tasks, documentType);
|
|
@@ -472,7 +590,7 @@ function App() {
|
|
|
472
590
|
else
|
|
473
591
|
setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: failed.errorMessage || '客户合同解析失败,请重新解析' });
|
|
474
592
|
}
|
|
475
|
-
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')) {
|
|
476
594
|
if (documentType === 'PURCHASE_CONTRACT') {
|
|
477
595
|
setPurchaseImportTask(null);
|
|
478
596
|
setPurchaseReviewDraft(null);
|
|
@@ -481,10 +599,10 @@ function App() {
|
|
|
481
599
|
setCustomerImportState({ status: 'IDLE' });
|
|
482
600
|
}
|
|
483
601
|
}
|
|
484
|
-
}, [data.importTasks.items, orchestrateContractImport]);
|
|
602
|
+
}, [data.importTasks.items, handleBackgroundImportTrackingError, orchestrateContractImport]);
|
|
485
603
|
const retryImport = React.useCallback(async (taskId) => {
|
|
486
604
|
try {
|
|
487
|
-
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
|
|
605
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState, notifyTaskRecoveryRefreshError);
|
|
488
606
|
const start = asActionResult(unwrapResponse(raw));
|
|
489
607
|
if (start.success === false)
|
|
490
608
|
throw actionResponseError(start, '重新解析失败');
|
|
@@ -495,10 +613,10 @@ function App() {
|
|
|
495
613
|
setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
|
|
496
614
|
throw error;
|
|
497
615
|
}
|
|
498
|
-
}, [loadTaskState]);
|
|
616
|
+
}, [loadTaskState, notifyTaskRecoveryRefreshError]);
|
|
499
617
|
const retryContractImport = React.useCallback(async (taskId) => {
|
|
500
618
|
try {
|
|
501
|
-
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
|
|
619
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState, notifyTaskRecoveryRefreshError);
|
|
502
620
|
const start = asActionResult(unwrapResponse(raw));
|
|
503
621
|
if (start.success === false)
|
|
504
622
|
throw actionResponseError(start, '重新解析失败');
|
|
@@ -508,7 +626,38 @@ function App() {
|
|
|
508
626
|
setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
|
|
509
627
|
throw error;
|
|
510
628
|
}
|
|
511
|
-
}, [loadTaskState]);
|
|
629
|
+
}, [loadTaskState, notifyTaskRecoveryRefreshError]);
|
|
630
|
+
const openImportTaskFromStatus = React.useCallback(async (task) => {
|
|
631
|
+
if (task.status === 'FAILED' || task.status === 'TIMED_OUT') {
|
|
632
|
+
if (task.documentType === 'PURCHASE_CONTRACT' || task.documentType === 'CUSTOMER_CONTRACT')
|
|
633
|
+
await retryContractImport(task.id);
|
|
634
|
+
else
|
|
635
|
+
await retryImport(task.id);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (task.status !== 'AWAITING_REVIEW')
|
|
639
|
+
return;
|
|
640
|
+
if (task.documentType === 'CONTROLLED_CATALOG' || task.documentType === 'SANCTION_CATALOG') {
|
|
641
|
+
const completion = asImportCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId: task.id } })));
|
|
642
|
+
setImportCompletions(current => ({ ...current, [completion.task.documentType]: completion }));
|
|
643
|
+
changeRouteRef.current({ type: 'page', page: completion.task.documentType === 'CONTROLLED_CATALOG' ? 'controlled-goods' : 'sanctioned-companies' });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const fullTask = asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId: task.id } }))).task;
|
|
647
|
+
activeReviewResultRef.current = fullTask.id;
|
|
648
|
+
if (fullTask.documentType === 'PURCHASE_CONTRACT') {
|
|
649
|
+
blobUrlOwnerRef.current.clear('purchase');
|
|
650
|
+
const draft = adaptPurchaseReview(fullTask);
|
|
651
|
+
setPurchaseImportTask(fullTask);
|
|
652
|
+
setPurchaseReviewDraft(await attachPurchaseSource(draft, fullTask, blobUrlOwnerRef.current));
|
|
653
|
+
changeRouteRef.current({ type: 'procurement', level: 'suppliers' });
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
blobUrlOwnerRef.current.clear('customer');
|
|
657
|
+
const review = adaptCustomerContractReview(fullTask.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: fullTask.id, sourceFileId: fullTask.sourceFileId ?? '', sourceFileName: fullTask.sourceFileName ?? '' });
|
|
658
|
+
setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: fullTask.id, review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
|
|
659
|
+
changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
|
|
660
|
+
}, [retryContractImport, retryImport]);
|
|
512
661
|
const applyCustomerContractReanalysisResult = React.useCallback(async (result) => {
|
|
513
662
|
let review;
|
|
514
663
|
if (result.review) {
|
|
@@ -533,7 +682,7 @@ function App() {
|
|
|
533
682
|
const reanalyzeCustomerContract = React.useCallback(async (input) => {
|
|
534
683
|
setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
|
|
535
684
|
try {
|
|
536
|
-
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);
|
|
537
686
|
const response = asActionResult(unwrapResponse(raw));
|
|
538
687
|
if (response.success === false || !response.data)
|
|
539
688
|
throw actionResponseError(response, 'AI 重新分析启动失败');
|
|
@@ -741,7 +890,7 @@ function App() {
|
|
|
741
890
|
}, []);
|
|
742
891
|
const queryProcurement = React.useCallback(async (_collection, query) => {
|
|
743
892
|
await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
|
|
744
|
-
}, [loadRoute]);
|
|
893
|
+
}, [loadImportTasksFallback, loadRoute]);
|
|
745
894
|
const queryCustomerContracts = React.useCallback(async (query) => {
|
|
746
895
|
await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
|
|
747
896
|
}, [loadRoute]);
|
|
@@ -792,7 +941,7 @@ function App() {
|
|
|
792
941
|
requests.finish(requestId);
|
|
793
942
|
setBusy(requests.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() > 0);
|
|
794
943
|
}
|
|
795
|
-
}, []);
|
|
944
|
+
}, [loadImportTasksFallback]);
|
|
796
945
|
const exportAnalytics = React.useCallback(async (_request) => {
|
|
797
946
|
setActionBusy(true);
|
|
798
947
|
try {
|
|
@@ -872,16 +1021,17 @@ function App() {
|
|
|
872
1021
|
changeRouteRef.current = changeRoute;
|
|
873
1022
|
const page = routePage(route);
|
|
874
1023
|
const meta = route.type === 'procurement' ? procurementPageMeta[route.level] : pageMeta[page];
|
|
1024
|
+
const taskStatusItems = React.useMemo(() => buildTaskStatusItems(data.importTasks.items, openImportTaskFromStatus), [data.importTasks.items, openImportTaskFromStatus]);
|
|
875
1025
|
const contractCommands = page === 'procurement' && route.type === 'procurement'
|
|
876
1026
|
? route.level === 'suppliers'
|
|
877
|
-
? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u91C7\u8D2D\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_purchase_contract', file, { documentType: 'PURCHASE_CONTRACT' }) })
|
|
1027
|
+
? _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' }) })
|
|
878
1028
|
: undefined
|
|
879
1029
|
: page === 'customer-contract-review'
|
|
880
|
-
? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u5BA2\u6237\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_customer_contract', file, { documentType: 'CUSTOMER_CONTRACT' }) })
|
|
1030
|
+
? _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' }) })
|
|
881
1031
|
: undefined;
|
|
882
1032
|
if (analyticsPrototypeVisible)
|
|
883
|
-
return (_jsx(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, onRouteChange: changeRoute, onRefresh: () => void load(), children: _jsx(AnalyticsPrototypeBoard, { onClose: () => setAnalyticsPrototypeVisible(false) }) }));
|
|
884
|
-
return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: page === 'home' ? undefined : meta.subtitle, 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)
|
|
1033
|
+
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) }) }));
|
|
1034
|
+
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)
|
|
885
1035
|
void querySalesFiles('generatedFiles', { page: 1, pageSize: 20, filters: {}, parameters: { customerContractId: activeSalesFileOrder.customerContractId, purchaseOrderId: activeSalesFileOrder.purchaseOrderId } }); } })] }));
|
|
886
1036
|
}
|
|
887
1037
|
function routePage(route) {
|
|
@@ -891,6 +1041,74 @@ function routePage(route) {
|
|
|
891
1041
|
return 'sales';
|
|
892
1042
|
return route.page;
|
|
893
1043
|
}
|
|
1044
|
+
function buildTaskStatusItems(tasks, onOpen) {
|
|
1045
|
+
return [...tasks]
|
|
1046
|
+
.sort((left, right) => String(right.updatedAt ?? right.queuedAt ?? right.createdAt ?? '').localeCompare(String(left.updatedAt ?? left.queuedAt ?? left.createdAt ?? '')))
|
|
1047
|
+
.map(task => {
|
|
1048
|
+
const tone = taskTone(task.status);
|
|
1049
|
+
return {
|
|
1050
|
+
id: task.id,
|
|
1051
|
+
title: task.sourceFileName || documentTypeLabel(task.documentType),
|
|
1052
|
+
subtitle: documentTypeLabel(task.documentType),
|
|
1053
|
+
status: task.status,
|
|
1054
|
+
statusLabel: taskStatusLabel(task.status),
|
|
1055
|
+
tone,
|
|
1056
|
+
updatedAt: formatTaskTime(task.updatedAt ?? task.queuedAt ?? task.createdAt),
|
|
1057
|
+
progressPercent: importTaskProgressPercent(task),
|
|
1058
|
+
...((task.status === 'AWAITING_REVIEW' || task.status === 'FAILED' || task.status === 'TIMED_OUT') ? { actionLabel: task.status === 'AWAITING_REVIEW' ? '审核' : '重试', onAction: () => onOpen(task) } : {})
|
|
1059
|
+
};
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
function taskTone(status) {
|
|
1063
|
+
if (status === 'AWAITING_REVIEW')
|
|
1064
|
+
return 'review';
|
|
1065
|
+
if (status === 'SUCCEEDED')
|
|
1066
|
+
return 'success';
|
|
1067
|
+
if (status === 'FAILED' || status === 'TIMED_OUT')
|
|
1068
|
+
return 'failed';
|
|
1069
|
+
if (status === 'QUEUED' || status === 'EXTRACTING' || status === 'AI_PARSING' || status === 'VALIDATING' || status === 'PERSISTING')
|
|
1070
|
+
return 'running';
|
|
1071
|
+
return 'idle';
|
|
1072
|
+
}
|
|
1073
|
+
function taskStatusLabel(status) {
|
|
1074
|
+
const labels = {
|
|
1075
|
+
QUEUED: '排队中', EXTRACTING: '提取中', AI_PARSING: 'AI 解析中', VALIDATING: '校验中', AWAITING_REVIEW: '待审核', PERSISTING: '入库中', SUCCEEDED: '已完成', FAILED: '失败', TIMED_OUT: '超时'
|
|
1076
|
+
};
|
|
1077
|
+
return labels[status] ?? status;
|
|
1078
|
+
}
|
|
1079
|
+
function documentTypeLabel(documentType) {
|
|
1080
|
+
const labels = {
|
|
1081
|
+
CONTROLLED_CATALOG: '管控商品目录', SANCTION_CATALOG: '公司制裁目录', PURCHASE_CONTRACT: '采购合同解析', CUSTOMER_CONTRACT: '客户合同解析'
|
|
1082
|
+
};
|
|
1083
|
+
return labels[documentType] ?? documentType;
|
|
1084
|
+
}
|
|
1085
|
+
function formatTaskTime(value) {
|
|
1086
|
+
if (typeof value !== 'string' || !value)
|
|
1087
|
+
return null;
|
|
1088
|
+
const date = new Date(value);
|
|
1089
|
+
if (!Number.isFinite(date.getTime()))
|
|
1090
|
+
return value;
|
|
1091
|
+
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, '/');
|
|
1092
|
+
}
|
|
1093
|
+
function mergeWorkbenchDataWithLocalTasks(current, incoming) {
|
|
1094
|
+
const items = mergeImportTaskItems(current.importTasks.items, incoming.importTasks.items);
|
|
1095
|
+
return {
|
|
1096
|
+
...incoming,
|
|
1097
|
+
importTasks: {
|
|
1098
|
+
...incoming.importTasks,
|
|
1099
|
+
items,
|
|
1100
|
+
total: Math.max(numberValue(incoming.importTasks.total, 0), incoming.importTasks.items.length, current.importTasks.items.length, items.length),
|
|
1101
|
+
page: numberValue(incoming.importTasks.page, numberValue(current.importTasks.page, 1)),
|
|
1102
|
+
pageSize: incoming.importTasks.pageSize ?? current.importTasks.pageSize ?? 100
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function hasLocalUploadPlaceholders(items) {
|
|
1107
|
+
return items.some(item => {
|
|
1108
|
+
const task = item && typeof item === 'object' && !Array.isArray(item) ? item : null;
|
|
1109
|
+
return task ? isLocalUploadPlaceholder(task) : false;
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
894
1112
|
function analyticsQueryFromRoute(page, pageSize, values) {
|
|
895
1113
|
const requestedPageSize = resolvePaginationPageSize(pageSize);
|
|
896
1114
|
const dateValue = values.orderDate;
|
|
@@ -931,6 +1149,26 @@ function actionResponseError(result, fallback) {
|
|
|
931
1149
|
function asRecord(value) {
|
|
932
1150
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
933
1151
|
}
|
|
1152
|
+
function summaryRequestParameters(value) {
|
|
1153
|
+
const parameters = { ...asRecord(value) };
|
|
1154
|
+
delete parameters.collection;
|
|
1155
|
+
delete parameters.filters;
|
|
1156
|
+
delete parameters.taskId;
|
|
1157
|
+
delete parameters.runId;
|
|
1158
|
+
delete parameters.customerContractId;
|
|
1159
|
+
delete parameters.purchaseOrderId;
|
|
1160
|
+
return parameters;
|
|
1161
|
+
}
|
|
1162
|
+
function summaryRequestQuery(parameters) {
|
|
1163
|
+
return { page: 1, pageSize: 100, search: nextSummaryRequestNonce(), parameters: summaryRequestParameters(parameters) };
|
|
1164
|
+
}
|
|
1165
|
+
function importTasksCollectionRequest() {
|
|
1166
|
+
return { page: 1, pageSize: 100, parameters: { collection: 'importTasks', filters: '{}', refreshNonce: nextSummaryRequestNonce() } };
|
|
1167
|
+
}
|
|
1168
|
+
function nextSummaryRequestNonce() {
|
|
1169
|
+
summaryRequestSequence += 1;
|
|
1170
|
+
return `__tcw_summary_refresh__:${Date.now()}:${summaryRequestSequence}`;
|
|
1171
|
+
}
|
|
934
1172
|
function numberValue(value, fallback) {
|
|
935
1173
|
const result = Number(value);
|
|
936
1174
|
return Number.isFinite(result) ? result : fallback;
|
|
@@ -952,26 +1190,11 @@ function downloadActionFile(value) {
|
|
|
952
1190
|
anchor.href = url;
|
|
953
1191
|
anchor.download = file.fileName;
|
|
954
1192
|
anchor.click();
|
|
955
|
-
setTimeout(() => URL.revokeObjectURL(url),
|
|
1193
|
+
setTimeout(() => URL.revokeObjectURL(url), DOWNLOAD_URL_REVOKE_DELAY_MS);
|
|
956
1194
|
}
|
|
957
|
-
function ContractUploadCommand({ label, busy, onFile }) {
|
|
1195
|
+
function ContractUploadCommand({ label, busy, onFile, onError }) {
|
|
958
1196
|
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)
|
|
959
|
-
void onFile(file).catch(
|
|
960
|
-
}
|
|
961
|
-
function adaptPurchaseReview(task) {
|
|
962
|
-
const payload = asRecord(task.resultPayload);
|
|
963
|
-
const supplier = asRecord(payload.supplier);
|
|
964
|
-
const order = asRecord(payload.order);
|
|
965
|
-
const lines = Array.isArray(payload.lines) ? payload.lines.map(value => {
|
|
966
|
-
const line = asRecord(value);
|
|
967
|
-
return { ...line, sourceType: line.sourceType === 'MANUAL_ENTRY' ? 'MANUAL_ENTRY' : 'CONTRACT_EXTRACTED', hsCodeStatus: line.hsCodeStatus === 'AI_CONFIRMED' || line.hsCodeStatus === 'MANUALLY_CONFIRMED' ? line.hsCodeStatus : 'UNCONFIRMED' };
|
|
968
|
-
}) : [];
|
|
969
|
-
return {
|
|
970
|
-
supplier: { ...supplier, name: String(supplier.name ?? '') },
|
|
971
|
-
order: { ...order, supplierId: String(order.supplierId ?? ''), contractNo: String(order.contractNo ?? ''), orderDate: String(order.orderDate ?? ''), contractAmountCny: String(order.contractAmountCny ?? '0') },
|
|
972
|
-
lines,
|
|
973
|
-
sourceFileName: task.sourceFileName ?? null
|
|
974
|
-
};
|
|
1197
|
+
void onFile(file).catch(error => onError?.(error instanceof Error ? error.message : '合同上传失败')); } })] });
|
|
975
1198
|
}
|
|
976
1199
|
async function attachPurchaseSource(draft, task, owner) {
|
|
977
1200
|
if (draft.sourceFileUrl || !task.sourceFileId)
|
|
@@ -1010,7 +1233,7 @@ async function loadSourcePreview(sourceFileId, slot, owner) {
|
|
|
1010
1233
|
throw new Error('源文件预览响应类型无效');
|
|
1011
1234
|
}
|
|
1012
1235
|
function textPreviewUrl(title, content) {
|
|
1013
|
-
const html = `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>${escapeHtml(title)}</title><style>body{margin:0;padding:
|
|
1236
|
+
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>`;
|
|
1014
1237
|
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
|
1015
1238
|
}
|
|
1016
1239
|
function escapeHtml(value) { return value.replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character] ?? character); }
|