@chenchaolong/plugin-trade-compliance-workbench 1.0.5 → 1.0.7
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/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/adapters/hsbianma.provider.d.ts +6 -8
- package/dist/lib/adapters/hsbianma.provider.d.ts.map +1 -1
- package/dist/lib/adapters/hsbianma.provider.js +159 -53
- package/dist/lib/adapters/hsbianma.provider.js.map +1 -1
- package/dist/lib/adapters/translation.provider.d.ts +7 -1
- package/dist/lib/adapters/translation.provider.d.ts.map +1 -1
- package/dist/lib/adapters/translation.provider.js +15 -5
- package/dist/lib/adapters/translation.provider.js.map +1 -1
- package/dist/lib/domain/catalog-localization.d.ts +14 -0
- package/dist/lib/domain/catalog-localization.d.ts.map +1 -0
- package/dist/lib/domain/catalog-localization.js +19 -0
- package/dist/lib/domain/catalog-localization.js.map +1 -0
- package/dist/lib/domain/file-name.d.ts +8 -0
- package/dist/lib/domain/file-name.d.ts.map +1 -0
- package/dist/lib/domain/file-name.js +111 -0
- package/dist/lib/domain/file-name.js.map +1 -0
- package/dist/lib/domain/import-issues.d.ts +4 -0
- package/dist/lib/domain/import-issues.d.ts.map +1 -1
- package/dist/lib/domain/import-issues.js +14 -3
- package/dist/lib/domain/import-issues.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/entities/customer.entity.d.ts +3 -0
- package/dist/lib/entities/customer.entity.d.ts.map +1 -1
- package/dist/lib/entities/customer.entity.js +12 -0
- package/dist/lib/entities/customer.entity.js.map +1 -1
- package/dist/lib/entities/shared.entity.d.ts +9 -0
- package/dist/lib/entities/shared.entity.d.ts.map +1 -1
- package/dist/lib/entities/shared.entity.js +33 -0
- package/dist/lib/entities/shared.entity.js.map +1 -1
- package/dist/lib/import-jobs/agent-handoff.dispatcher.d.ts +15 -0
- package/dist/lib/import-jobs/agent-handoff.dispatcher.d.ts.map +1 -0
- package/dist/lib/import-jobs/agent-handoff.dispatcher.js +114 -0
- package/dist/lib/import-jobs/agent-handoff.dispatcher.js.map +1 -0
- package/dist/lib/import-jobs/contracts.d.ts +76 -0
- package/dist/lib/import-jobs/contracts.d.ts.map +1 -0
- package/dist/lib/import-jobs/contracts.js +31 -0
- package/dist/lib/import-jobs/contracts.js.map +1 -0
- package/dist/lib/import-jobs/import-command.builder.d.ts +30 -0
- package/dist/lib/import-jobs/import-command.builder.d.ts.map +1 -0
- package/dist/lib/import-jobs/import-command.builder.js +97 -0
- package/dist/lib/import-jobs/import-command.builder.js.map +1 -0
- package/dist/lib/import-jobs/import-job-callback.processor.d.ts +15 -0
- package/dist/lib/import-jobs/import-job-callback.processor.d.ts.map +1 -0
- package/dist/lib/import-jobs/import-job-callback.processor.js +107 -0
- package/dist/lib/import-jobs/import-job-callback.processor.js.map +1 -0
- package/dist/lib/import-jobs/import-job.coordinator.d.ts +17 -0
- package/dist/lib/import-jobs/import-job.coordinator.d.ts.map +1 -0
- package/dist/lib/import-jobs/import-job.coordinator.js +256 -0
- package/dist/lib/import-jobs/import-job.coordinator.js.map +1 -0
- package/dist/lib/import-jobs/import-job.processor.d.ts +24 -0
- package/dist/lib/import-jobs/import-job.processor.d.ts.map +1 -0
- package/dist/lib/import-jobs/import-job.processor.js +213 -0
- package/dist/lib/import-jobs/import-job.processor.js.map +1 -0
- package/dist/lib/import-jobs/import-timeout.processor.d.ts +12 -0
- package/dist/lib/import-jobs/import-timeout.processor.d.ts.map +1 -0
- package/dist/lib/import-jobs/import-timeout.processor.js +37 -0
- package/dist/lib/import-jobs/import-timeout.processor.js.map +1 -0
- package/dist/lib/import-jobs/organization-lease.d.ts +39 -0
- package/dist/lib/import-jobs/organization-lease.d.ts.map +1 -0
- package/dist/lib/import-jobs/organization-lease.js +136 -0
- package/dist/lib/import-jobs/organization-lease.js.map +1 -0
- package/dist/lib/plugin-context.token.d.ts +4 -0
- package/dist/lib/plugin-context.token.d.ts.map +1 -0
- package/dist/lib/plugin-context.token.js +2 -0
- package/dist/lib/plugin-context.token.js.map +1 -0
- package/dist/lib/public-dto.d.ts +6 -0
- package/dist/lib/public-dto.d.ts.map +1 -0
- package/dist/lib/public-dto.js +33 -0
- package/dist/lib/public-dto.js.map +1 -0
- 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/action-execution.d.ts +26 -0
- package/dist/lib/remote-ui/action-execution.d.ts.map +1 -0
- package/dist/lib/remote-ui/action-execution.js +85 -0
- package/dist/lib/remote-ui/action-execution.js.map +1 -0
- package/dist/lib/remote-ui/analytics-contracts.d.ts +3 -2
- package/dist/lib/remote-ui/analytics-contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/analytics-contracts.js.map +1 -1
- package/dist/lib/remote-ui/app-source.d.ts.map +1 -1
- package/dist/lib/remote-ui/app-source.js +426 -169
- package/dist/lib/remote-ui/app-source.js.map +1 -1
- package/dist/lib/remote-ui/company-logo-lifecycle.d.ts +14 -0
- package/dist/lib/remote-ui/company-logo-lifecycle.d.ts.map +1 -0
- package/dist/lib/remote-ui/company-logo-lifecycle.js +52 -0
- package/dist/lib/remote-ui/company-logo-lifecycle.js.map +1 -0
- package/dist/lib/remote-ui/components/data-grid.d.ts +9 -4
- package/dist/lib/remote-ui/components/data-grid.d.ts.map +1 -1
- package/dist/lib/remote-ui/components/data-grid.js +41 -9
- package/dist/lib/remote-ui/components/data-grid.js.map +1 -1
- package/dist/lib/remote-ui/components/forms.d.ts +6 -1
- package/dist/lib/remote-ui/components/forms.d.ts.map +1 -1
- package/dist/lib/remote-ui/components/forms.js +6 -1
- package/dist/lib/remote-ui/components/forms.js.map +1 -1
- package/dist/lib/remote-ui/components/layout.d.ts.map +1 -1
- package/dist/lib/remote-ui/components/layout.js +3 -3
- package/dist/lib/remote-ui/components/layout.js.map +1 -1
- package/dist/lib/remote-ui/contract-import.d.ts +37 -32
- package/dist/lib/remote-ui/contract-import.d.ts.map +1 -1
- package/dist/lib/remote-ui/contract-import.js +51 -59
- package/dist/lib/remote-ui/contract-import.js.map +1 -1
- package/dist/lib/remote-ui/contracts.d.ts +20 -27
- package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/contracts.js +8 -0
- package/dist/lib/remote-ui/contracts.js.map +1 -1
- package/dist/lib/remote-ui/customer-contract-contracts.d.ts +5 -11
- package/dist/lib/remote-ui/customer-contract-contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/customer-contract-contracts.js.map +1 -1
- package/dist/lib/remote-ui/date-time.d.ts +3 -0
- package/dist/lib/remote-ui/date-time.d.ts.map +1 -0
- package/dist/lib/remote-ui/date-time.js +58 -0
- package/dist/lib/remote-ui/date-time.js.map +1 -0
- package/dist/lib/remote-ui/model.d.ts +26 -1
- package/dist/lib/remote-ui/model.d.ts.map +1 -1
- package/dist/lib/remote-ui/model.js +151 -15
- package/dist/lib/remote-ui/model.js.map +1 -1
- package/dist/lib/remote-ui/pages/analytics.d.ts +2 -0
- package/dist/lib/remote-ui/pages/analytics.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/analytics.js +11 -6
- package/dist/lib/remote-ui/pages/analytics.js.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.d.ts +37 -14
- package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.js +149 -91
- package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
- package/dist/lib/remote-ui/pages/contracts.d.ts +11 -6
- package/dist/lib/remote-ui/pages/contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/contracts.js +45 -33
- package/dist/lib/remote-ui/pages/contracts.js.map +1 -1
- package/dist/lib/remote-ui/pages/files.d.ts +29 -3
- package/dist/lib/remote-ui/pages/files.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/files.js +60 -16
- package/dist/lib/remote-ui/pages/files.js.map +1 -1
- package/dist/lib/remote-ui/pages/home.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/home.js +18 -18
- package/dist/lib/remote-ui/pages/home.js.map +1 -1
- package/dist/lib/remote-ui/pages/procurement.d.ts +29 -4
- package/dist/lib/remote-ui/pages/procurement.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/procurement.js +58 -63
- package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
- package/dist/lib/remote-ui/pages/sales.d.ts +47 -1
- package/dist/lib/remote-ui/pages/sales.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/sales.js +60 -20
- package/dist/lib/remote-ui/pages/sales.js.map +1 -1
- package/dist/lib/remote-ui/pages/settings.d.ts +34 -1
- package/dist/lib/remote-ui/pages/settings.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/settings.js +120 -94
- package/dist/lib/remote-ui/pages/settings.js.map +1 -1
- package/dist/lib/remote-ui/polling.d.ts +41 -0
- package/dist/lib/remote-ui/polling.d.ts.map +1 -0
- package/dist/lib/remote-ui/polling.js +148 -0
- package/dist/lib/remote-ui/polling.js.map +1 -0
- package/dist/lib/remote-ui/procurement-contracts.d.ts +4 -9
- package/dist/lib/remote-ui/procurement-contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/sales-contracts.d.ts +2 -2
- package/dist/lib/remote-ui/sales-contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/settings-contracts.d.ts +9 -1
- package/dist/lib/remote-ui/settings-contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/settings-contracts.js.map +1 -1
- package/dist/lib/remote-ui/toast-queue.d.ts +29 -0
- package/dist/lib/remote-ui/toast-queue.d.ts.map +1 -0
- package/dist/lib/remote-ui/toast-queue.js +71 -0
- package/dist/lib/remote-ui/toast-queue.js.map +1 -0
- package/dist/lib/sales-file.service.d.ts.map +1 -1
- package/dist/lib/sales-file.service.js +5 -5
- package/dist/lib/sales-file.service.js.map +1 -1
- package/dist/lib/types.d.ts +5 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +3 -0
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/workbench-action-contract.d.ts +4 -0
- package/dist/lib/workbench-action-contract.d.ts.map +1 -0
- package/dist/lib/workbench-action-contract.js +43 -0
- package/dist/lib/workbench-action-contract.js.map +1 -0
- package/dist/lib/workbench-view.provider.d.ts +57 -78
- package/dist/lib/workbench-view.provider.d.ts.map +1 -1
- package/dist/lib/workbench-view.provider.js +89 -91
- package/dist/lib/workbench-view.provider.js.map +1 -1
- package/dist/lib/workbench.middleware.d.ts +129 -72
- package/dist/lib/workbench.middleware.d.ts.map +1 -1
- package/dist/lib/workbench.middleware.js +16 -3
- package/dist/lib/workbench.middleware.js.map +1 -1
- package/dist/lib/workbench.plugin.d.ts +3 -1
- package/dist/lib/workbench.plugin.d.ts.map +1 -1
- package/dist/lib/workbench.plugin.js +13 -4
- package/dist/lib/workbench.plugin.js.map +1 -1
- package/dist/lib/workbench.service.d.ts +136 -40
- package/dist/lib/workbench.service.d.ts.map +1 -1
- package/dist/lib/workbench.service.js +985 -71
- package/dist/lib/workbench.service.js.map +1 -1
- package/package.json +6 -5
|
@@ -2,12 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { createRoot } from 'react-dom/client';
|
|
4
4
|
import { createRemoteBridge } from './bridge.js';
|
|
5
|
-
import {
|
|
5
|
+
import { resolvePaginationPageSize } from './contracts.js';
|
|
6
|
+
import { clampPageAfterSingleDeletion, createBlobUrlOwner, createKeyedLatestRequestGate, createLatestCommittedRequest, createLatestRequestGate, generatedFileHistoryKey, normalizeWorkbenchData, routeDataRequest, runKeyedLatestRequest, unwrapResponse } from './model.js';
|
|
6
7
|
import { encodeViewFilters } from '../view-parameters.js';
|
|
7
8
|
import { InfoNotice } from './components/data-grid.js';
|
|
8
|
-
import { LoadingBlocker,
|
|
9
|
+
import { LoadingBlocker, ToastStack } from './components/forms.js';
|
|
10
|
+
import { addToast, createToastId, createToastTimerRegistry, removeToast } from './toast-queue.js';
|
|
9
11
|
import { buildBreadcrumbs, WorkbenchShell } from './components/layout.js';
|
|
10
|
-
import { CatalogCommands, CatalogImportError, ControlledGoodsPage,
|
|
12
|
+
import { CatalogCommands, CatalogImportError, ControlledGoodsPage, findActiveCatalogImportTasks, findLatestFailedCatalogImportTask, runCatalogImportOrchestration, SanctionedCompaniesPage } from './pages/catalogs.js';
|
|
11
13
|
import { HomePage } from './pages/home.js';
|
|
12
14
|
import { ProcurementPage } from './pages/procurement.js';
|
|
13
15
|
import { CustomerContractsPage, adaptCustomerContractReview } from './pages/contracts.js';
|
|
@@ -15,7 +17,9 @@ import { SalesPage } from './pages/sales.js';
|
|
|
15
17
|
import { SalesFilesPage } from './pages/files.js';
|
|
16
18
|
import { AnalyticsPage } from './pages/analytics.js';
|
|
17
19
|
import { SettingsPage } from './pages/settings.js';
|
|
18
|
-
import { ContractImportError, findRecoverableContractImport,
|
|
20
|
+
import { ContractImportError, findRecoverableContractImport, findResumableContractImports, runContractImportOrchestration, runCustomerContractReanalysis, taskTransition } from './contract-import.js';
|
|
21
|
+
import { compareTaskBusinessOrder, createPendingResultQueue, createPollingTaskTracker, isAbortError, isPollingObservationTimeout, submitAndRefreshTaskState } from './polling.js';
|
|
22
|
+
import { executeWorkbenchAction } from './action-execution.js';
|
|
19
23
|
import './styles.css';
|
|
20
24
|
const pageMeta = {
|
|
21
25
|
home: { title: '首页', subtitle: '查看系统风险操作提示与外贸合规业务流程。' },
|
|
@@ -30,24 +34,48 @@ const pageMeta = {
|
|
|
30
34
|
};
|
|
31
35
|
const bridge = createRemoteBridge();
|
|
32
36
|
const initialAnalyticsQuery = { mode: 'PURCHASE', dimension: 'SUPPLIER', filters: {}, page: 1, pageSize: 20 };
|
|
37
|
+
function comparePendingReviewBusinessOrder(left, right) {
|
|
38
|
+
const leftTask = left.kind === 'CONTRACT_IMPORT' ? left.completion.task : left.result.run;
|
|
39
|
+
const rightTask = right.kind === 'CONTRACT_IMPORT' ? right.completion.task : right.result.run;
|
|
40
|
+
return compareTaskBusinessOrder(leftTask, rightTask);
|
|
41
|
+
}
|
|
42
|
+
class ActionResponseError extends Error {
|
|
43
|
+
constructor(message, code) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.name = 'ActionResponseError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
33
49
|
let excelEditorRuntimePromise = null;
|
|
34
50
|
function App() {
|
|
35
51
|
const [route, setRoute] = React.useState({ type: 'page', page: 'home' });
|
|
36
52
|
const routeRef = React.useRef(route);
|
|
37
53
|
const contextRef = React.useRef(null);
|
|
38
54
|
const requestGateRef = React.useRef(createLatestRequestGate());
|
|
55
|
+
const taskStateRequestGateRef = React.useRef(createLatestRequestGate());
|
|
56
|
+
const taskRecoveryTimerRef = React.useRef(null);
|
|
39
57
|
const routeRequestGateRef = React.useRef(createLatestRequestGate());
|
|
58
|
+
const settingsRatesRequestGateRef = React.useRef(createLatestRequestGate());
|
|
59
|
+
const generatedFilesRequestGateRef = React.useRef(createKeyedLatestRequestGate());
|
|
60
|
+
const activeGeneratedFilesKeyRef = React.useRef(null);
|
|
61
|
+
const settingsRatesQueryRef = React.useRef({ page: 1, pageSize: 20 });
|
|
40
62
|
const analyticsRequestsRef = React.useRef(createLatestCommittedRequest(initialAnalyticsQuery));
|
|
41
63
|
const [analyticsCommittedQuery, setAnalyticsCommittedQuery] = React.useState(initialAnalyticsQuery);
|
|
42
64
|
const [data, setData] = React.useState(() => normalizeWorkbenchData(null));
|
|
43
65
|
const [busy, setBusy] = React.useState(false);
|
|
44
66
|
const [actionBusy, setActionBusy] = React.useState(false);
|
|
45
67
|
const [catalogCommand, setCatalogCommand] = React.useState(null);
|
|
46
|
-
const [
|
|
68
|
+
const [importCompletions, setImportCompletions] = React.useState({ CONTROLLED_CATALOG: null, SANCTION_CATALOG: null });
|
|
47
69
|
const [importFailures, setImportFailures] = React.useState({ CONTROLLED_CATALOG: null, SANCTION_CATALOG: null });
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
70
|
+
const catalogTaskTrackerRef = React.useRef(createPollingTaskTracker());
|
|
71
|
+
const contractTaskTrackerRef = React.useRef(createPollingTaskTracker());
|
|
72
|
+
const reanalysisTaskTrackerRef = React.useRef(createPollingTaskTracker());
|
|
73
|
+
const handledContractTasksRef = React.useRef(new Set());
|
|
74
|
+
const handledReanalysisResultsRef = React.useRef(new Set());
|
|
75
|
+
const pendingReviewResultsRef = React.useRef(createPendingResultQueue(item => item.kind === 'CONTRACT_IMPORT' ? item.completion.task.id : item.result.run.id, comparePendingReviewBusinessOrder));
|
|
76
|
+
const activeReviewResultRef = React.useRef(null);
|
|
77
|
+
const presentNextReviewRef = React.useRef(() => undefined);
|
|
78
|
+
const reviewPresentationTimerRef = React.useRef(null);
|
|
51
79
|
const blobUrlOwnerRef = React.useRef(createBlobUrlOwner());
|
|
52
80
|
const changeRouteRef = React.useRef(() => undefined);
|
|
53
81
|
const [procurementContext, setProcurementContext] = React.useState({ supplier: null, order: null, metrics: null });
|
|
@@ -57,15 +85,24 @@ function App() {
|
|
|
57
85
|
const [customerReanalysisState, setCustomerReanalysisState] = React.useState({ status: 'IDLE' });
|
|
58
86
|
const [activeSalesFileOrder, setActiveSalesFileOrder] = React.useState(null);
|
|
59
87
|
const [editingSalesFile, setEditingSalesFile] = React.useState(null);
|
|
60
|
-
const [
|
|
88
|
+
const [toasts, setToasts] = React.useState([]);
|
|
89
|
+
const [toastTimers] = React.useState(() => createToastTimerRegistry((id) => setToasts((items) => removeToast(items, id))));
|
|
61
90
|
const [analyticsError, setAnalyticsError] = React.useState(null);
|
|
91
|
+
const setToast = React.useCallback((toast) => {
|
|
92
|
+
setToasts((items) => addToast(items, toast, createToastId));
|
|
93
|
+
}, []);
|
|
94
|
+
const closeToast = React.useCallback((id) => {
|
|
95
|
+
toastTimers.dismiss(id);
|
|
96
|
+
setToasts((items) => removeToast(items, id));
|
|
97
|
+
}, [toastTimers]);
|
|
62
98
|
const loadRoute = React.useCallback(async (targetRoute, page = 1, pageSize = 20, filters = {}) => {
|
|
63
|
-
const
|
|
99
|
+
const requestedPageSize = resolvePaginationPageSize(pageSize);
|
|
100
|
+
const request = routeDataRequest(targetRoute, page, requestedPageSize, filters);
|
|
64
101
|
if (!request)
|
|
65
102
|
return;
|
|
66
103
|
const collection = String(asRecord(request.parameters).collection);
|
|
67
104
|
const requests = collection === 'analytics' ? analyticsRequestsRef.current : routeRequestGateRef.current;
|
|
68
|
-
const analyticsCandidate = collection === 'analytics' ? analyticsQueryFromRoute(page,
|
|
105
|
+
const analyticsCandidate = collection === 'analytics' ? analyticsQueryFromRoute(page, requestedPageSize, filters) : null;
|
|
69
106
|
const requestId = analyticsCandidate ? analyticsRequestsRef.current.begin(analyticsCandidate) : routeRequestGateRef.current.begin();
|
|
70
107
|
setBusy(true);
|
|
71
108
|
try {
|
|
@@ -78,15 +115,18 @@ function App() {
|
|
|
78
115
|
setAnalyticsCommittedQuery(analyticsRequestsRef.current.committed());
|
|
79
116
|
setData(current => ({ ...current, analytics: normalizeWorkbenchData({ summary: { analytics: payload } }).analytics }));
|
|
80
117
|
}
|
|
81
|
-
else if (collection === 'settings')
|
|
82
|
-
|
|
118
|
+
else if (collection === 'settings') {
|
|
119
|
+
const settings = normalizeWorkbenchData({ summary: { settings: payload } }).settings;
|
|
120
|
+
settingsRatesQueryRef.current = { page: settings.rates.page, pageSize: settings.rates.pageSize };
|
|
121
|
+
setData(current => ({ ...current, settings }));
|
|
122
|
+
}
|
|
83
123
|
else
|
|
84
124
|
setData(current => ({
|
|
85
125
|
...current,
|
|
86
126
|
[collection]: {
|
|
87
127
|
...payload,
|
|
88
128
|
items: Array.isArray(payload.items) ? payload.items : [],
|
|
89
|
-
total: numberValue(payload.total, 0), page: numberValue(payload.page, page), pageSize:
|
|
129
|
+
total: numberValue(payload.total, 0), page: numberValue(payload.page, page), pageSize: requestedPageSize
|
|
90
130
|
}
|
|
91
131
|
}));
|
|
92
132
|
if (targetRoute.type === 'procurement')
|
|
@@ -116,7 +156,7 @@ function App() {
|
|
|
116
156
|
const requestId = gate.begin();
|
|
117
157
|
setBusy(true);
|
|
118
158
|
try {
|
|
119
|
-
const response = await bridge.requestData({ page: 1, pageSize:
|
|
159
|
+
const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
|
|
120
160
|
if (gate.isLatest(requestId)) {
|
|
121
161
|
const normalized = normalizeWorkbenchData(response);
|
|
122
162
|
bridge.configure({ uploadPolicy: normalized.uploadPolicy });
|
|
@@ -133,6 +173,64 @@ function App() {
|
|
|
133
173
|
setBusy(gate.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0);
|
|
134
174
|
}
|
|
135
175
|
}, [loadRoute]);
|
|
176
|
+
const loadTaskState = React.useCallback(async () => {
|
|
177
|
+
const active = contextRef.current;
|
|
178
|
+
if (!active)
|
|
179
|
+
return false;
|
|
180
|
+
const gate = taskStateRequestGateRef.current;
|
|
181
|
+
const requestId = gate.begin();
|
|
182
|
+
try {
|
|
183
|
+
const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
|
|
184
|
+
if (!gate.commit(requestId))
|
|
185
|
+
return false;
|
|
186
|
+
const normalized = normalizeWorkbenchData(response);
|
|
187
|
+
setData(current => ({
|
|
188
|
+
...current,
|
|
189
|
+
controlledGoods: normalized.controlledGoods,
|
|
190
|
+
controlledBatches: normalized.controlledBatches,
|
|
191
|
+
sanctionedCompanies: normalized.sanctionedCompanies,
|
|
192
|
+
sanctionBatches: normalized.sanctionBatches,
|
|
193
|
+
importTasks: normalized.importTasks,
|
|
194
|
+
customerContractAnalyses: normalized.customerContractAnalyses
|
|
195
|
+
}));
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (gate.isLatest(requestId))
|
|
200
|
+
setToast({ message: error instanceof Error ? `任务状态刷新失败:${error.message}` : '任务状态刷新失败', tone: 'error' });
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
gate.finish(requestId);
|
|
205
|
+
}
|
|
206
|
+
}, []);
|
|
207
|
+
const scheduleTaskStateRecovery = React.useCallback(() => {
|
|
208
|
+
if (taskRecoveryTimerRef.current != null)
|
|
209
|
+
return;
|
|
210
|
+
taskRecoveryTimerRef.current = window.setInterval(() => {
|
|
211
|
+
void loadTaskState().then(refreshed => {
|
|
212
|
+
if (!refreshed || taskRecoveryTimerRef.current == null)
|
|
213
|
+
return;
|
|
214
|
+
window.clearInterval(taskRecoveryTimerRef.current);
|
|
215
|
+
taskRecoveryTimerRef.current = null;
|
|
216
|
+
});
|
|
217
|
+
}, 2000);
|
|
218
|
+
}, [loadTaskState]);
|
|
219
|
+
React.useEffect(() => () => {
|
|
220
|
+
if (taskRecoveryTimerRef.current != null)
|
|
221
|
+
window.clearInterval(taskRecoveryTimerRef.current);
|
|
222
|
+
if (reviewPresentationTimerRef.current != null)
|
|
223
|
+
window.clearTimeout(reviewPresentationTimerRef.current);
|
|
224
|
+
}, []);
|
|
225
|
+
React.useEffect(() => {
|
|
226
|
+
return () => catalogTaskTrackerRef.current.abortAll();
|
|
227
|
+
}, []);
|
|
228
|
+
React.useEffect(() => {
|
|
229
|
+
return () => contractTaskTrackerRef.current.abortAll();
|
|
230
|
+
}, []);
|
|
231
|
+
React.useEffect(() => {
|
|
232
|
+
return () => reanalysisTaskTrackerRef.current.abortAll();
|
|
233
|
+
}, []);
|
|
136
234
|
React.useEffect(() => {
|
|
137
235
|
bridge.start({
|
|
138
236
|
onInit(nextContext) {
|
|
@@ -148,84 +246,87 @@ function App() {
|
|
|
148
246
|
});
|
|
149
247
|
return () => { blobUrlOwnerRef.current.dispose(); bridge.destroy(); };
|
|
150
248
|
}, [load]);
|
|
249
|
+
React.useEffect(() => () => toastTimers.dispose(), [toastTimers]);
|
|
151
250
|
React.useEffect(() => {
|
|
152
251
|
const timer = window.setTimeout(() => bridge.reportResize(), 0);
|
|
153
252
|
return () => window.clearTimeout(timer);
|
|
154
253
|
}, [route, data, busy]);
|
|
155
254
|
React.useEffect(() => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const timer = window.setTimeout(() => setToast(null), 4200);
|
|
159
|
-
return () => window.clearTimeout(timer);
|
|
160
|
-
}, [toast]);
|
|
255
|
+
toastTimers.sync(toasts);
|
|
256
|
+
}, [toastTimers, toasts]);
|
|
161
257
|
const executeAction = React.useCallback(async (actionKey, input) => {
|
|
162
258
|
setActionBusy(true);
|
|
163
259
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
setToast({ message: error instanceof Error ? error.message : '操作失败', tone: 'error' });
|
|
176
|
-
throw error;
|
|
260
|
+
return await executeWorkbenchAction({
|
|
261
|
+
actionKey,
|
|
262
|
+
mutate: async () => asActionResult(unwrapResponse(await bridge.executeAction(actionKey, input))),
|
|
263
|
+
refresh: load,
|
|
264
|
+
notify: setToast,
|
|
265
|
+
resolveMessage: resolveActionMessage,
|
|
266
|
+
onResult: result => { if (actionKey === 'download_import_issues' || actionKey === 'download_sales_file')
|
|
267
|
+
downloadActionFile(result.data); }
|
|
268
|
+
});
|
|
177
269
|
}
|
|
178
270
|
finally {
|
|
179
271
|
setActionBusy(false);
|
|
180
272
|
}
|
|
181
273
|
}, [load]);
|
|
182
274
|
const executeSalesFileAction = React.useCallback((actionKey, input) => executeAction(actionKey, input), [executeAction]);
|
|
183
|
-
const
|
|
275
|
+
const schedulePendingReviewPresentation = React.useCallback(() => {
|
|
276
|
+
if (reviewPresentationTimerRef.current != null)
|
|
277
|
+
return;
|
|
278
|
+
reviewPresentationTimerRef.current = window.setTimeout(() => {
|
|
279
|
+
reviewPresentationTimerRef.current = null;
|
|
280
|
+
presentNextReviewRef.current();
|
|
281
|
+
}, 0);
|
|
282
|
+
}, []);
|
|
283
|
+
const enqueuePendingReview = React.useCallback((result) => {
|
|
284
|
+
pendingReviewResultsRef.current.enqueue(result);
|
|
285
|
+
schedulePendingReviewPresentation();
|
|
286
|
+
}, [schedulePendingReviewPresentation]);
|
|
287
|
+
const orchestrateImport = React.useCallback(async (start, signal) => {
|
|
184
288
|
const taskId = start.data?.task.id;
|
|
185
289
|
if (!taskId)
|
|
186
290
|
throw new Error('解析响应缺少任务标识');
|
|
187
|
-
if (runningImportTaskRef.current === taskId)
|
|
188
|
-
throw new Error('该目录任务正在解析中');
|
|
189
|
-
runningImportTaskRef.current = taskId;
|
|
190
|
-
setActionBusy(true);
|
|
191
291
|
const documentType = start.data?.task.documentType;
|
|
192
292
|
if (documentType)
|
|
193
293
|
setImportFailures((current) => ({ ...current, [documentType]: null }));
|
|
194
294
|
try {
|
|
195
295
|
const completion = await runCatalogImportOrchestration({
|
|
196
296
|
start,
|
|
197
|
-
invokeClientCommand: (commandKey, payload) => bridge.invokeClientCommand(commandKey, payload),
|
|
198
|
-
markCommandDispatched: async (taskId) => {
|
|
199
|
-
const result = asActionResult(unwrapResponse(await bridge.executeAction('mark_import_command_dispatched', { taskId })));
|
|
200
|
-
if (result.success === false)
|
|
201
|
-
throw new Error(resolveActionMessage(result) || '记录解析命令状态失败');
|
|
202
|
-
},
|
|
203
297
|
pollTask: async (taskId) => asImportCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
|
|
204
|
-
refresh:
|
|
298
|
+
refresh: loadTaskState,
|
|
299
|
+
signal,
|
|
300
|
+
onTransition: (previous, next) => {
|
|
301
|
+
const notice = taskTransition(previous, next);
|
|
302
|
+
if (notice)
|
|
303
|
+
setToast(notice);
|
|
304
|
+
},
|
|
305
|
+
onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
|
|
205
306
|
});
|
|
206
|
-
|
|
207
|
-
setToast({ message: '目录导入完成', tone: 'success' });
|
|
307
|
+
setImportCompletions(current => ({ ...current, [completion.task.documentType]: completion }));
|
|
208
308
|
return completion;
|
|
209
309
|
}
|
|
210
310
|
catch (error) {
|
|
211
|
-
if (error
|
|
311
|
+
if (isAbortError(error))
|
|
312
|
+
throw error;
|
|
313
|
+
if (isPollingObservationTimeout(error)) {
|
|
314
|
+
setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
|
|
315
|
+
scheduleTaskStateRecovery();
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
if (error instanceof CatalogImportError) {
|
|
212
319
|
setImportFailures((current) => ({ ...current, [error.task.documentType]: error.task }));
|
|
213
|
-
|
|
320
|
+
}
|
|
321
|
+
else
|
|
322
|
+
setToast({ message: error instanceof Error ? error.message : '目录解析失败', tone: 'error' });
|
|
214
323
|
throw error;
|
|
215
324
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
setActionBusy(false);
|
|
219
|
-
}
|
|
220
|
-
}, [load]);
|
|
221
|
-
const orchestrateContractImport = React.useCallback(async (start) => {
|
|
325
|
+
}, [loadTaskState, scheduleTaskStateRecovery]);
|
|
326
|
+
const orchestrateContractImport = React.useCallback(async (start, signal) => {
|
|
222
327
|
const task = start.data?.task;
|
|
223
328
|
if (!task)
|
|
224
329
|
throw new Error('解析响应缺少合同任务标识');
|
|
225
|
-
if (runningContractTasksRef.current.has(task.id))
|
|
226
|
-
throw new Error('该合同任务正在解析中');
|
|
227
|
-
runningContractTasksRef.current.add(task.id);
|
|
228
|
-
setActionBusy(true);
|
|
229
330
|
if (task.documentType === 'PURCHASE_CONTRACT') {
|
|
230
331
|
blobUrlOwnerRef.current.clear('purchase');
|
|
231
332
|
setPurchaseImportTask(task);
|
|
@@ -238,29 +339,28 @@ function App() {
|
|
|
238
339
|
try {
|
|
239
340
|
const completion = await runContractImportOrchestration({
|
|
240
341
|
start,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
342
|
+
signal,
|
|
343
|
+
pollTask: async (taskId) => asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
|
|
344
|
+
onTransition: (previous, next) => {
|
|
345
|
+
const notice = taskTransition(previous, next);
|
|
346
|
+
if (notice)
|
|
347
|
+
setToast(notice);
|
|
246
348
|
},
|
|
247
|
-
|
|
349
|
+
onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
|
|
248
350
|
});
|
|
249
|
-
handledContractTasksRef.current[task.documentType] = task.id;
|
|
250
351
|
if (completion.task.status === 'AWAITING_REVIEW') {
|
|
251
|
-
|
|
252
|
-
const draft = adaptPurchaseReview(completion.task);
|
|
253
|
-
setPurchaseImportTask(completion.task);
|
|
254
|
-
setPurchaseReviewDraft(await attachPurchaseSource(draft, completion.task, blobUrlOwnerRef.current));
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
const review = adaptCustomerContractReview(completion.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: completion.task.id, sourceFileId: completion.task.sourceFileId ?? '', sourceFileName: completion.task.sourceFileName ?? '' });
|
|
258
|
-
setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: completion.task.id, review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
|
|
259
|
-
}
|
|
352
|
+
enqueuePendingReview({ kind: 'CONTRACT_IMPORT', completion });
|
|
260
353
|
}
|
|
261
354
|
return completion;
|
|
262
355
|
}
|
|
263
356
|
catch (error) {
|
|
357
|
+
if (isAbortError(error))
|
|
358
|
+
throw error;
|
|
359
|
+
if (isPollingObservationTimeout(error)) {
|
|
360
|
+
setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
|
|
361
|
+
scheduleTaskStateRecovery();
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
264
364
|
const failed = error instanceof ContractImportError ? error.task : task;
|
|
265
365
|
if (task.documentType === 'PURCHASE_CONTRACT') {
|
|
266
366
|
setPurchaseImportTask({ ...failed, status: failed.status === 'TIMED_OUT' ? 'TIMED_OUT' : 'FAILED', errorMessage: error instanceof Error ? error.message : '采购合同解析失败' });
|
|
@@ -268,62 +368,58 @@ function App() {
|
|
|
268
368
|
}
|
|
269
369
|
else
|
|
270
370
|
setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: error instanceof Error ? error.message : '客户合同解析失败' });
|
|
271
|
-
|
|
371
|
+
if (!(error instanceof ContractImportError))
|
|
372
|
+
setToast({ message: error instanceof Error ? error.message : '合同解析失败', tone: 'error' });
|
|
272
373
|
throw error;
|
|
273
374
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
setActionBusy(false);
|
|
277
|
-
}
|
|
278
|
-
}, []);
|
|
279
|
-
const executeFileAction = React.useCallback(async (actionKey, file, input) => {
|
|
375
|
+
}, [enqueuePendingReview, scheduleTaskStateRecovery]);
|
|
376
|
+
const executeFileAction = React.useCallback(async (actionKey, file, input, notifyError = true) => {
|
|
280
377
|
try {
|
|
281
|
-
const
|
|
378
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeFileAction(actionKey, file, input), loadTaskState);
|
|
379
|
+
const start = asActionResult(unwrapResponse(raw));
|
|
282
380
|
if (start.success === false)
|
|
283
|
-
throw
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return { success: true, data: completion, message: '合同解析完成,等待审核', refresh: false };
|
|
287
|
-
}
|
|
288
|
-
const completion = await orchestrateImport(start);
|
|
289
|
-
return { success: true, data: completion, message: '目录导入完成', refresh: true };
|
|
381
|
+
throw actionResponseError(start, '上传失败');
|
|
382
|
+
setToast({ message: resolveActionMessage(start) || '解析任务已提交', tone: 'info' });
|
|
383
|
+
return start;
|
|
290
384
|
}
|
|
291
385
|
catch (error) {
|
|
292
|
-
if (!(error instanceof CatalogImportError))
|
|
386
|
+
if (notifyError && !(error instanceof CatalogImportError))
|
|
293
387
|
setToast({ message: error instanceof Error ? error.message : '上传失败', tone: 'error' });
|
|
294
388
|
throw error;
|
|
295
389
|
}
|
|
296
|
-
}, [
|
|
390
|
+
}, [loadTaskState]);
|
|
391
|
+
const executeCatalogFileAction = React.useCallback((actionKey, file, input) => executeFileAction(actionKey, file, input, false), [executeFileAction]);
|
|
297
392
|
React.useEffect(() => {
|
|
298
393
|
const tasks = data.importTasks.items;
|
|
299
394
|
setImportFailures({
|
|
300
395
|
CONTROLLED_CATALOG: findLatestFailedCatalogImportTask(tasks, 'CONTROLLED_CATALOG'),
|
|
301
396
|
SANCTION_CATALOG: findLatestFailedCatalogImportTask(tasks, 'SANCTION_CATALOG')
|
|
302
397
|
});
|
|
303
|
-
const active
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
398
|
+
for (const active of findActiveCatalogImportTasks(tasks)) {
|
|
399
|
+
const running = catalogTaskTrackerRef.current.run(active.id, signal => orchestrateImport({ success: true, data: { task: active } }, signal));
|
|
400
|
+
if (running)
|
|
401
|
+
void running.catch(() => undefined);
|
|
307
402
|
}
|
|
308
403
|
}, [data.importTasks.items, orchestrateImport]);
|
|
309
404
|
React.useEffect(() => {
|
|
310
405
|
const tasks = data.importTasks.items;
|
|
406
|
+
for (const resumable of findResumableContractImports(tasks)) {
|
|
407
|
+
if (handledContractTasksRef.current.has(resumable.id))
|
|
408
|
+
continue;
|
|
409
|
+
const running = contractTaskTrackerRef.current.run(resumable.id, signal => orchestrateContractImport({ success: true, data: { task: resumable } }, signal));
|
|
410
|
+
if (running)
|
|
411
|
+
void running.catch(() => undefined);
|
|
412
|
+
}
|
|
311
413
|
for (const documentType of ['PURCHASE_CONTRACT', 'CUSTOMER_CONTRACT']) {
|
|
312
|
-
const resumable = findResumableContractImport(tasks, documentType);
|
|
313
414
|
const failed = findRecoverableContractImport(tasks, documentType);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
void orchestrateContractImport({ success: true, data: { task: resumable, commands: resumable.executionCommand ? [resumable.executionCommand] : [], polling: { intervalMs: 2000, timeoutMs: 1200000 } } }).catch(() => undefined);
|
|
317
|
-
}
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
if (!resumable && failed) {
|
|
415
|
+
const hasResumable = tasks.some(task => task.documentType === documentType && (task.status === 'AWAITING_REVIEW' || ['QUEUED', 'EXTRACTING', 'AI_PARSING', 'VALIDATING', 'PERSISTING'].includes(task.status)));
|
|
416
|
+
if (!hasResumable && failed) {
|
|
321
417
|
if (documentType === 'PURCHASE_CONTRACT')
|
|
322
418
|
setPurchaseImportTask(failed);
|
|
323
419
|
else
|
|
324
420
|
setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: failed.errorMessage || '客户合同解析失败,请重新解析' });
|
|
325
421
|
}
|
|
326
|
-
if (!
|
|
422
|
+
if (!hasResumable && !failed && tasks.some(item => item.documentType === documentType && item.status === 'SUCCEEDED')) {
|
|
327
423
|
if (documentType === 'PURCHASE_CONTRACT') {
|
|
328
424
|
setPurchaseImportTask(null);
|
|
329
425
|
setPurchaseReviewDraft(null);
|
|
@@ -335,65 +431,173 @@ function App() {
|
|
|
335
431
|
}, [data.importTasks.items, orchestrateContractImport]);
|
|
336
432
|
const retryImport = React.useCallback(async (taskId) => {
|
|
337
433
|
try {
|
|
338
|
-
const
|
|
434
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
|
|
435
|
+
const start = asActionResult(unwrapResponse(raw));
|
|
339
436
|
if (start.success === false)
|
|
340
|
-
throw
|
|
341
|
-
|
|
437
|
+
throw actionResponseError(start, '重新解析失败');
|
|
438
|
+
setToast({ message: resolveActionMessage(start) || '重新解析任务已提交', tone: 'info' });
|
|
342
439
|
}
|
|
343
440
|
catch (error) {
|
|
344
441
|
if (!(error instanceof CatalogImportError))
|
|
345
442
|
setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
|
|
346
443
|
throw error;
|
|
347
444
|
}
|
|
348
|
-
}, [
|
|
445
|
+
}, [loadTaskState]);
|
|
349
446
|
const retryContractImport = React.useCallback(async (taskId) => {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
447
|
+
try {
|
|
448
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
|
|
449
|
+
const start = asActionResult(unwrapResponse(raw));
|
|
450
|
+
if (start.success === false)
|
|
451
|
+
throw actionResponseError(start, '重新解析失败');
|
|
452
|
+
setToast({ message: resolveActionMessage(start) || '重新解析任务已提交', tone: 'info' });
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
}, [loadTaskState]);
|
|
459
|
+
const applyCustomerContractReanalysisResult = React.useCallback(async (result) => {
|
|
460
|
+
let review;
|
|
461
|
+
if (result.review) {
|
|
462
|
+
review = adaptCustomerContractReview(result.review, { reviewSource: 'EXISTING_CONTRACT' });
|
|
463
|
+
}
|
|
464
|
+
else if (result.task) {
|
|
465
|
+
review = adaptCustomerContractReview(result.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: result.task.id, sourceFileId: result.task.sourceFileId ?? '', sourceFileName: result.task.sourceFileName ?? '' });
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
throw new Error('重新分析结果缺少合同审核数据');
|
|
469
|
+
}
|
|
470
|
+
const attached = await attachCustomerSource(review, blobUrlOwnerRef.current);
|
|
471
|
+
setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: result.task?.id ?? result.run.importTaskId ?? '', review: attached });
|
|
472
|
+
changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
|
|
473
|
+
return attached;
|
|
474
|
+
}, []);
|
|
475
|
+
const consumeCustomerContractReanalysisResult = React.useCallback(async (runId) => {
|
|
476
|
+
const consumed = asActionResult(unwrapResponse(await bridge.executeAction('consume_customer_contract_reanalysis_result', { runId })));
|
|
477
|
+
if (consumed.success === false)
|
|
478
|
+
throw new Error(resolveActionMessage(consumed) || '确认重新分析结果失败');
|
|
479
|
+
}, []);
|
|
358
480
|
const reanalyzeCustomerContract = React.useCallback(async (input) => {
|
|
359
481
|
setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
|
|
360
482
|
try {
|
|
361
|
-
const
|
|
483
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('reanalyze_customer_contract', { reviewSource: input.reviewSource, ...(input.customerContractId ? { id: input.customerContractId } : {}), ...(input.importTaskId ? { importTaskId: input.importTaskId } : {}) }), loadTaskState);
|
|
484
|
+
const response = asActionResult(unwrapResponse(raw));
|
|
362
485
|
if (response.success === false || !response.data)
|
|
363
|
-
throw
|
|
364
|
-
|
|
486
|
+
throw actionResponseError(response, 'AI 重新分析启动失败');
|
|
487
|
+
setToast({ message: resolveActionMessage(response) || 'AI 重新分析任务已提交', tone: 'info' });
|
|
488
|
+
const running = reanalysisTaskTrackerRef.current.run(response.data.run.id, signal => runCustomerContractReanalysis({
|
|
365
489
|
start: response.data,
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
},
|
|
377
|
-
poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } })))
|
|
378
|
-
});
|
|
379
|
-
let review;
|
|
380
|
-
if (result.review) {
|
|
381
|
-
review = adaptCustomerContractReview(result.review, { reviewSource: 'EXISTING_CONTRACT' });
|
|
382
|
-
}
|
|
383
|
-
else if (result.task) {
|
|
384
|
-
review = adaptCustomerContractReview(result.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: result.task.id, sourceFileId: result.task.sourceFileId ?? '', sourceFileName: result.task.sourceFileName ?? '' });
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
throw new Error('重新分析结果缺少合同审核数据');
|
|
388
|
-
}
|
|
490
|
+
signal,
|
|
491
|
+
poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } }))),
|
|
492
|
+
onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
|
|
493
|
+
}));
|
|
494
|
+
if (!running)
|
|
495
|
+
throw new Error('该 AI 重新分析任务正在跟踪中');
|
|
496
|
+
const result = await running;
|
|
497
|
+
const review = await applyCustomerContractReanalysisResult(result);
|
|
498
|
+
await consumeCustomerContractReanalysisResult(result.run.id);
|
|
499
|
+
handledReanalysisResultsRef.current.add(result.run.id);
|
|
389
500
|
setCustomerReanalysisState({ status: 'IDLE' });
|
|
501
|
+
setToast({ message: 'AI 重新分析完成', tone: 'success' });
|
|
502
|
+
await loadTaskState();
|
|
390
503
|
return review;
|
|
391
504
|
}
|
|
392
505
|
catch (error) {
|
|
506
|
+
if (isAbortError(error))
|
|
507
|
+
throw error;
|
|
508
|
+
if (error instanceof ActionResponseError && error.code === 'IMPORT_QUEUE_UNAVAILABLE') {
|
|
509
|
+
setCustomerReanalysisState({ status: 'RUNNING', message: '任务已保存,等待队列服务恢复' });
|
|
510
|
+
setToast({ message: error.message, tone: 'info' });
|
|
511
|
+
scheduleTaskStateRecovery();
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
if (isPollingObservationTimeout(error)) {
|
|
515
|
+
setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析仍在后台处理中' });
|
|
516
|
+
setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
|
|
517
|
+
scheduleTaskStateRecovery();
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
393
520
|
setCustomerReanalysisState({ status: 'FAILED', message: error instanceof Error ? error.message : 'AI 重新分析失败' });
|
|
521
|
+
setToast({ message: error instanceof Error ? error.message : 'AI 重新分析失败', tone: 'error' });
|
|
394
522
|
throw error;
|
|
395
523
|
}
|
|
396
|
-
}, []);
|
|
524
|
+
}, [applyCustomerContractReanalysisResult, consumeCustomerContractReanalysisResult, loadTaskState, scheduleTaskStateRecovery]);
|
|
525
|
+
React.useEffect(() => {
|
|
526
|
+
for (const recovery of data.customerContractAnalyses.items) {
|
|
527
|
+
const recoverable = recovery.recoveryType === 'ACTIVE_POLL' || recovery.recoveryType === 'SUCCEEDED_RESULT';
|
|
528
|
+
if (!recoverable)
|
|
529
|
+
continue;
|
|
530
|
+
if (handledReanalysisResultsRef.current.has(recovery.run.id))
|
|
531
|
+
continue;
|
|
532
|
+
const running = reanalysisTaskTrackerRef.current.run(recovery.run.id, signal => runCustomerContractReanalysis({
|
|
533
|
+
start: { run: recovery.run },
|
|
534
|
+
signal,
|
|
535
|
+
poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } }))),
|
|
536
|
+
onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
|
|
537
|
+
}));
|
|
538
|
+
if (!running)
|
|
539
|
+
continue;
|
|
540
|
+
setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
|
|
541
|
+
setToast({ message: '已恢复 AI 重新分析任务', tone: 'info' });
|
|
542
|
+
void running.then(result => {
|
|
543
|
+
enqueuePendingReview({ kind: 'REANALYSIS', result });
|
|
544
|
+
}).catch(error => {
|
|
545
|
+
if (isAbortError(error))
|
|
546
|
+
return;
|
|
547
|
+
if (isPollingObservationTimeout(error)) {
|
|
548
|
+
setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析仍在后台处理中' });
|
|
549
|
+
setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
|
|
550
|
+
scheduleTaskStateRecovery();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
setCustomerReanalysisState({ status: 'FAILED', message: error instanceof Error ? error.message : 'AI 重新分析失败' });
|
|
554
|
+
setToast({ message: error instanceof Error ? error.message : 'AI 重新分析失败', tone: 'error' });
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}, [data.customerContractAnalyses.items, enqueuePendingReview, scheduleTaskStateRecovery]);
|
|
558
|
+
const presentNextPendingReview = React.useCallback(async () => {
|
|
559
|
+
if (activeReviewResultRef.current)
|
|
560
|
+
return;
|
|
561
|
+
const pending = pendingReviewResultsRef.current.peek();
|
|
562
|
+
if (!pending)
|
|
563
|
+
return;
|
|
564
|
+
const resultId = pending.kind === 'CONTRACT_IMPORT' ? pending.completion.task.id : pending.result.run.id;
|
|
565
|
+
activeReviewResultRef.current = resultId;
|
|
566
|
+
try {
|
|
567
|
+
if (pending.kind === 'CONTRACT_IMPORT') {
|
|
568
|
+
const task = pending.completion.task;
|
|
569
|
+
if (task.documentType === 'PURCHASE_CONTRACT') {
|
|
570
|
+
blobUrlOwnerRef.current.clear('purchase');
|
|
571
|
+
const draft = adaptPurchaseReview(task);
|
|
572
|
+
setPurchaseImportTask(task);
|
|
573
|
+
setPurchaseReviewDraft(await attachPurchaseSource(draft, task, blobUrlOwnerRef.current));
|
|
574
|
+
changeRouteRef.current({ type: 'procurement', level: 'suppliers' });
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
blobUrlOwnerRef.current.clear('customer');
|
|
578
|
+
const review = adaptCustomerContractReview(task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: task.id, sourceFileId: task.sourceFileId ?? '', sourceFileName: task.sourceFileName ?? '' });
|
|
579
|
+
setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: task.id, review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
|
|
580
|
+
changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
|
|
581
|
+
}
|
|
582
|
+
pendingReviewResultsRef.current.ack(resultId);
|
|
583
|
+
handledContractTasksRef.current.add(resultId);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
await applyCustomerContractReanalysisResult(pending.result);
|
|
587
|
+
await consumeCustomerContractReanalysisResult(pending.result.run.id);
|
|
588
|
+
pendingReviewResultsRef.current.ack(resultId);
|
|
589
|
+
handledReanalysisResultsRef.current.add(pending.result.run.id);
|
|
590
|
+
setCustomerReanalysisState({ status: 'IDLE' });
|
|
591
|
+
setToast({ message: 'AI 重新分析完成', tone: 'success' });
|
|
592
|
+
await loadTaskState();
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
activeReviewResultRef.current = null;
|
|
596
|
+
setToast({ message: error instanceof Error ? error.message : '审核结果恢复失败', tone: 'error' });
|
|
597
|
+
scheduleTaskStateRecovery();
|
|
598
|
+
}
|
|
599
|
+
}, [applyCustomerContractReanalysisResult, consumeCustomerContractReanalysisResult, loadTaskState, scheduleTaskStateRecovery]);
|
|
600
|
+
presentNextReviewRef.current = () => { void presentNextPendingReview(); };
|
|
397
601
|
const loadCustomerContractReview = React.useCallback(async (customerContractId) => {
|
|
398
602
|
const result = asActionResult(unwrapResponse(await bridge.executeAction('get_customer_contract_review', { id: customerContractId })));
|
|
399
603
|
if (result.success === false || !result.data)
|
|
@@ -405,21 +609,27 @@ function App() {
|
|
|
405
609
|
const closePurchaseReview = React.useCallback(() => {
|
|
406
610
|
blobUrlOwnerRef.current.clear('purchase');
|
|
407
611
|
setPurchaseReviewDraft(null);
|
|
408
|
-
|
|
612
|
+
activeReviewResultRef.current = null;
|
|
613
|
+
schedulePendingReviewPresentation();
|
|
614
|
+
}, [schedulePendingReviewPresentation]);
|
|
409
615
|
const closeCustomerReview = React.useCallback(() => {
|
|
410
616
|
blobUrlOwnerRef.current.clear('customer');
|
|
411
617
|
setCustomerImportState({ status: 'IDLE' });
|
|
412
|
-
|
|
618
|
+
activeReviewResultRef.current = null;
|
|
619
|
+
schedulePendingReviewPresentation();
|
|
620
|
+
}, [schedulePendingReviewPresentation]);
|
|
413
621
|
const queryImportIssues = React.useCallback(async (query) => {
|
|
414
|
-
const
|
|
415
|
-
|
|
622
|
+
const requestedPageSize = resolvePaginationPageSize(query.pageSize);
|
|
623
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection: 'importIssues', documentType: query.documentType, batchId: query.batchId, issueType: query.issueType } })));
|
|
624
|
+
return { items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: requestedPageSize };
|
|
416
625
|
}, []);
|
|
417
626
|
const queryCatalog = React.useCallback(async (collection, query) => {
|
|
627
|
+
const requestedPageSize = resolvePaginationPageSize(query.pageSize);
|
|
418
628
|
const gate = requestGateRef.current;
|
|
419
629
|
const requestId = gate.begin();
|
|
420
630
|
setBusy(true);
|
|
421
631
|
try {
|
|
422
|
-
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize:
|
|
632
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection, filters: encodeViewFilters(query.filters) } })));
|
|
423
633
|
if (!gate.isLatest(requestId))
|
|
424
634
|
return;
|
|
425
635
|
setData((current) => ({
|
|
@@ -429,7 +639,7 @@ function App() {
|
|
|
429
639
|
items: Array.isArray(payload.items) ? payload.items : [],
|
|
430
640
|
total: numberValue(payload.total, 0),
|
|
431
641
|
page: numberValue(payload.page, query.page),
|
|
432
|
-
pageSize:
|
|
642
|
+
pageSize: requestedPageSize
|
|
433
643
|
}
|
|
434
644
|
}));
|
|
435
645
|
}
|
|
@@ -442,6 +652,27 @@ function App() {
|
|
|
442
652
|
setBusy(gate.activeCount() > 0);
|
|
443
653
|
}
|
|
444
654
|
}, []);
|
|
655
|
+
const queryMonthlyRates = React.useCallback(async (query) => {
|
|
656
|
+
const requestedPageSize = resolvePaginationPageSize(query.pageSize);
|
|
657
|
+
const gate = settingsRatesRequestGateRef.current;
|
|
658
|
+
const requestId = gate.begin();
|
|
659
|
+
setBusy(true);
|
|
660
|
+
try {
|
|
661
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection: 'monthlyRates' } })));
|
|
662
|
+
if (!gate.commit(requestId))
|
|
663
|
+
return;
|
|
664
|
+
settingsRatesQueryRef.current = { page: numberValue(payload.page, query.page), pageSize: requestedPageSize };
|
|
665
|
+
setData(current => ({ ...current, settings: normalizeWorkbenchData({ summary: { settings: { ...current.settings, rates: { ...payload, pageSize: requestedPageSize } } } }).settings }));
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
if (gate.isLatest(requestId))
|
|
669
|
+
setToast({ message: error instanceof Error ? `月度汇率加载失败:${error.message}` : '月度汇率加载失败', tone: 'error' });
|
|
670
|
+
}
|
|
671
|
+
finally {
|
|
672
|
+
gate.finish(requestId);
|
|
673
|
+
setBusy(gate.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() > 0);
|
|
674
|
+
}
|
|
675
|
+
}, []);
|
|
445
676
|
const queryProcurement = React.useCallback(async (_collection, query) => {
|
|
446
677
|
await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
|
|
447
678
|
}, [loadRoute]);
|
|
@@ -452,20 +683,36 @@ function App() {
|
|
|
452
683
|
await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
|
|
453
684
|
}, [loadRoute]);
|
|
454
685
|
const querySalesFiles = React.useCallback(async (collection, query) => {
|
|
686
|
+
const requestedPageSize = resolvePaginationPageSize(query.pageSize);
|
|
455
687
|
if (collection === 'salesFileOrders') {
|
|
456
|
-
await loadRoute({ type: 'page', page: 'sales-files' }, query.page,
|
|
688
|
+
await loadRoute({ type: 'page', page: 'sales-files' }, query.page, requestedPageSize, query.filters);
|
|
457
689
|
return;
|
|
458
690
|
}
|
|
459
|
-
const
|
|
460
|
-
|
|
691
|
+
const key = generatedFileHistoryKey(query.parameters);
|
|
692
|
+
if (!key) {
|
|
693
|
+
setToast({ message: '历史生成文件查询缺少合同或采购订单标识', tone: 'error' });
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
activeGeneratedFilesKeyRef.current = key;
|
|
697
|
+
await runKeyedLatestRequest({
|
|
698
|
+
gate: generatedFilesRequestGateRef.current,
|
|
699
|
+
key,
|
|
700
|
+
request: async () => asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection, ...query.parameters, filters: encodeViewFilters(query.filters) } }))),
|
|
701
|
+
isCurrent: () => activeGeneratedFilesKeyRef.current === key,
|
|
702
|
+
commit: payload => setData(current => ({ ...current, generatedFiles: { ...payload, items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: requestedPageSize } })),
|
|
703
|
+
onError: error => setToast({ message: error instanceof Error ? `历史生成文件加载失败:${error.message}` : '历史生成文件加载失败', tone: 'error' }),
|
|
704
|
+
onStart: () => setBusy(true),
|
|
705
|
+
onFinish: () => setBusy(generatedFilesRequestGateRef.current.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0)
|
|
706
|
+
});
|
|
461
707
|
}, [loadRoute]);
|
|
462
708
|
const queryAnalytics = React.useCallback(async (query) => {
|
|
709
|
+
const requestedQuery = { ...query, pageSize: resolvePaginationPageSize(query.pageSize) };
|
|
463
710
|
const requests = analyticsRequestsRef.current;
|
|
464
|
-
const requestId = requests.begin(
|
|
711
|
+
const requestId = requests.begin(requestedQuery);
|
|
465
712
|
setBusy(true);
|
|
466
713
|
setAnalyticsError(null);
|
|
467
714
|
try {
|
|
468
|
-
const payload = asRecord(unwrapResponse(await bridge.requestData({ page:
|
|
715
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: requestedQuery.page, pageSize: requestedQuery.pageSize, parameters: { collection: 'analytics', mode: requestedQuery.mode, dimension: requestedQuery.dimension, filters: encodeViewFilters(requestedQuery.filters) } })));
|
|
469
716
|
if (!requests.commit(requestId))
|
|
470
717
|
return;
|
|
471
718
|
setAnalyticsCommittedQuery(requests.committed());
|
|
@@ -509,25 +756,26 @@ function App() {
|
|
|
509
756
|
const result = asActionResult(unwrapResponse(await bridge.executeAction('save_settings', request)));
|
|
510
757
|
if (result.success === false)
|
|
511
758
|
throw new Error(resolveActionMessage(result) || '设置保存失败');
|
|
512
|
-
await loadRoute({ type: 'page', page: 'settings' });
|
|
759
|
+
await loadRoute({ type: 'page', page: 'settings' }, settingsRatesQueryRef.current.page, settingsRatesQueryRef.current.pageSize);
|
|
513
760
|
}, [loadRoute]);
|
|
514
761
|
const restoreSettings = React.useCallback(async (request) => {
|
|
515
762
|
const result = asActionResult(unwrapResponse(await bridge.executeAction('restore_settings_default', request)));
|
|
516
763
|
if (result.success === false)
|
|
517
764
|
throw new Error(resolveActionMessage(result) || '恢复系统默认失败');
|
|
518
|
-
await loadRoute({ type: 'page', page: 'settings' });
|
|
765
|
+
await loadRoute({ type: 'page', page: 'settings' }, settingsRatesQueryRef.current.page, settingsRatesQueryRef.current.pageSize);
|
|
519
766
|
}, [loadRoute]);
|
|
520
767
|
const deleteSettingsRate = React.useCallback(async (request) => {
|
|
521
768
|
const result = asActionResult(unwrapResponse(await bridge.executeAction('delete_settings_rate', request)));
|
|
522
769
|
if (result.success === false)
|
|
523
770
|
throw new Error(resolveActionMessage(result) || '月度汇率删除失败');
|
|
524
|
-
|
|
525
|
-
|
|
771
|
+
const { page, pageSize } = settingsRatesQueryRef.current;
|
|
772
|
+
const clampedPage = clampPageAfterSingleDeletion(page, pageSize, data.settings.rates.total);
|
|
773
|
+
await loadRoute({ type: 'page', page: 'settings' }, clampedPage, pageSize);
|
|
774
|
+
}, [data.settings.rates.total, loadRoute]);
|
|
526
775
|
const uploadCompanyLogo = React.useCallback(async (file) => {
|
|
527
776
|
const result = asActionResult(unwrapResponse(await bridge.executeFileAction('upload_company_logo', file, {})));
|
|
528
777
|
if (result.success === false || !result.data?.logoFileId)
|
|
529
778
|
throw new Error(resolveActionMessage(result) || '公司 Logo 上传失败');
|
|
530
|
-
setToast({ message: '公司 Logo 上传成功', tone: 'success' });
|
|
531
779
|
return result.data.logoFileId;
|
|
532
780
|
}, []);
|
|
533
781
|
const discardCompanyLogo = React.useCallback(async (logoFileId) => {
|
|
@@ -540,6 +788,13 @@ function App() {
|
|
|
540
788
|
if (order)
|
|
541
789
|
changeRouteRef.current({ type: 'page', page: 'sales-files' });
|
|
542
790
|
}, []);
|
|
791
|
+
const selectSalesFileOrder = React.useCallback((order) => {
|
|
792
|
+
const previousKey = activeGeneratedFilesKeyRef.current;
|
|
793
|
+
if (previousKey)
|
|
794
|
+
generatedFilesRequestGateRef.current.invalidate(previousKey);
|
|
795
|
+
activeGeneratedFilesKeyRef.current = order ? generatedFileHistoryKey(order) : null;
|
|
796
|
+
setActiveSalesFileOrder(order);
|
|
797
|
+
}, []);
|
|
543
798
|
const changeRoute = React.useCallback((nextRoute) => {
|
|
544
799
|
routeRef.current = nextRoute;
|
|
545
800
|
setRoute(nextRoute);
|
|
@@ -553,7 +808,7 @@ function App() {
|
|
|
553
808
|
: page === 'customer-contract-review'
|
|
554
809
|
? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u5BA2\u6237\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_customer_contract', file, { documentType: 'CUSTOMER_CONTRACT' }) })
|
|
555
810
|
: undefined;
|
|
556
|
-
return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: 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:
|
|
811
|
+
return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: 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 }), page === 'sales-files' && _jsx(SalesFilesPage, { orders: data.salesFileOrders, history: data.generatedFiles, activeOrder: activeSalesFileOrder, 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)
|
|
557
812
|
void querySalesFiles('generatedFiles', { page: 1, pageSize: 20, filters: {}, parameters: { customerContractId: activeSalesFileOrder.customerContractId, purchaseOrderId: activeSalesFileOrder.purchaseOrderId } }); } })] }));
|
|
558
813
|
}
|
|
559
814
|
function routePage(route) {
|
|
@@ -564,6 +819,7 @@ function routePage(route) {
|
|
|
564
819
|
return route.page;
|
|
565
820
|
}
|
|
566
821
|
function analyticsQueryFromRoute(page, pageSize, values) {
|
|
822
|
+
const requestedPageSize = resolvePaginationPageSize(pageSize);
|
|
567
823
|
const dateValue = values.orderDate;
|
|
568
824
|
const date = dateValue && typeof dateValue === 'object' && !Array.isArray(dateValue) ? dateValue : null;
|
|
569
825
|
const orderDate = date ? { from: optionalText(date.from), to: optionalText(date.to) } : undefined;
|
|
@@ -574,11 +830,11 @@ function analyticsQueryFromRoute(page, pageSize, values) {
|
|
|
574
830
|
};
|
|
575
831
|
if (values.mode === 'SALES')
|
|
576
832
|
return {
|
|
577
|
-
mode: 'SALES', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'CUSTOMER', page, pageSize,
|
|
833
|
+
mode: 'SALES', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'CUSTOMER', page, pageSize: requestedPageSize,
|
|
578
834
|
filters: { ...common, ...(optionalText(values.customer) ? { customer: optionalText(values.customer) } : {}), ...(enabled(values.unreceivedOnly) ? { unreceivedOnly: true } : {}) }
|
|
579
835
|
};
|
|
580
836
|
return {
|
|
581
|
-
mode: 'PURCHASE', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'SUPPLIER', page, pageSize,
|
|
837
|
+
mode: 'PURCHASE', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'SUPPLIER', page, pageSize: requestedPageSize,
|
|
582
838
|
filters: { ...common, ...(optionalText(values.supplier) ? { supplier: optionalText(values.supplier) } : {}), ...(enabled(values.unpaidOnly) ? { unpaidOnly: true } : {}), ...(enabled(values.unrefundedOnly) ? { unrefundedOnly: true } : {}) }
|
|
583
839
|
};
|
|
584
840
|
}
|
|
@@ -596,6 +852,9 @@ function resolveActionMessage(result) {
|
|
|
596
852
|
}
|
|
597
853
|
return '';
|
|
598
854
|
}
|
|
855
|
+
function actionResponseError(result, fallback) {
|
|
856
|
+
return new ActionResponseError(resolveActionMessage(result) || fallback, result.error?.code);
|
|
857
|
+
}
|
|
599
858
|
function asRecord(value) {
|
|
600
859
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
601
860
|
}
|
|
@@ -694,9 +953,6 @@ function asCustomerContractReanalysisResult(value) {
|
|
|
694
953
|
throw new Error('轮询响应缺少重新分析任务');
|
|
695
954
|
return payload;
|
|
696
955
|
}
|
|
697
|
-
function shouldRefreshAfterAction(actionKey) {
|
|
698
|
-
return new Set(['save_controlled_goods', 'update_controlled_goods', 'save_sanctioned_company', 'update_sanctioned_company', 'save_purchase_review', 'save_customer_contract_review', 'update_supplier', 'update_purchase_order', 'add_purchase_line', 'update_purchase_line', 'update_customer_contract_review', 'delete_entity']).has(actionKey);
|
|
699
|
-
}
|
|
700
956
|
async function ensureExcelEditorRuntime() {
|
|
701
957
|
const runtimeWindow = window;
|
|
702
958
|
if (runtimeWindow.TradeComplianceExcelEditor?.open)
|
|
@@ -737,7 +993,7 @@ async function ensureExcelEditorRuntime() {
|
|
|
737
993
|
});
|
|
738
994
|
return excelEditorRuntimePromise;
|
|
739
995
|
}
|
|
740
|
-
function SalesFileEditor({ file, onClose, onSaved }) {
|
|
996
|
+
function SalesFileEditor({ file, onClose, onSaved, onNotify }) {
|
|
741
997
|
const containerRef = React.useRef(null);
|
|
742
998
|
const editorRef = React.useRef(null);
|
|
743
999
|
const [error, setError] = React.useState('');
|
|
@@ -778,12 +1034,13 @@ function SalesFileEditor({ file, onClose, onSaved }) {
|
|
|
778
1034
|
return () => { cancelled = true; editorRef.current?.dispose(); editorRef.current = null; };
|
|
779
1035
|
}, [file.id]);
|
|
780
1036
|
const save = async () => { if (!editorRef.current)
|
|
781
|
-
return; setSaving(true);
|
|
1037
|
+
return; setSaving(true); try {
|
|
782
1038
|
await editorRef.current.save();
|
|
1039
|
+
onNotify({ message: '工作簿保存成功', tone: 'success' });
|
|
783
1040
|
onSaved();
|
|
784
1041
|
}
|
|
785
1042
|
catch (cause) {
|
|
786
|
-
|
|
1043
|
+
onNotify({ message: cause instanceof Error ? cause.message : '工作簿保存失败', tone: 'error' });
|
|
787
1044
|
}
|
|
788
1045
|
finally {
|
|
789
1046
|
setSaving(false);
|