@chenchaolong/plugin-trade-compliance-workbench 1.0.6 → 1.0.8
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 +462 -176
- 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 +4 -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 +6 -0
- package/dist/lib/remote-ui/pages/analytics.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/analytics.js +56 -6
- package/dist/lib/remote-ui/pages/analytics.js.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.d.ts +35 -14
- package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.js +139 -89
- package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
- package/dist/lib/remote-ui/pages/contracts.d.ts +10 -5
- package/dist/lib/remote-ui/pages/contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/contracts.js +29 -26
- package/dist/lib/remote-ui/pages/contracts.js.map +1 -1
- package/dist/lib/remote-ui/pages/files.d.ts +55 -4
- package/dist/lib/remote-ui/pages/files.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/files.js +121 -14
- package/dist/lib/remote-ui/pages/files.js.map +1 -1
- package/dist/lib/remote-ui/pages/home.js +2 -2
- package/dist/lib/remote-ui/pages/home.js.map +1 -1
- package/dist/lib/remote-ui/pages/procurement.d.ts +6 -4
- package/dist/lib/remote-ui/pages/procurement.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/procurement.js +69 -66
- package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
- package/dist/lib/remote-ui/pages/sales.d.ts +51 -2
- package/dist/lib/remote-ui/pages/sales.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/sales.js +106 -19
- package/dist/lib/remote-ui/pages/sales.js.map +1 -1
- package/dist/lib/remote-ui/pages/settings.d.ts +29 -14
- package/dist/lib/remote-ui/pages/settings.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/settings.js +76 -69
- 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 +5 -3
- 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 +13 -35
- package/dist/lib/sales-file.service.d.ts.map +1 -1
- package/dist/lib/sales-file.service.js +39 -21
- package/dist/lib/sales-file.service.js.map +1 -1
- package/dist/lib/types.d.ts +22 -3
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +5 -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 +104 -94
- 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,20 +2,24 @@ 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';
|
|
14
16
|
import { SalesPage } from './pages/sales.js';
|
|
15
17
|
import { SalesFilesPage } from './pages/files.js';
|
|
16
|
-
import { AnalyticsPage } from './pages/analytics.js';
|
|
18
|
+
import { AnalyticsPage, AnalyticsPrototypeBoard } 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,50 @@ 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);
|
|
64
|
+
const analyticsPrototypeListenerCleanupRef = React.useRef(null);
|
|
65
|
+
const [analyticsPrototypeVisible, setAnalyticsPrototypeVisible] = React.useState(false);
|
|
42
66
|
const [data, setData] = React.useState(() => normalizeWorkbenchData(null));
|
|
43
67
|
const [busy, setBusy] = React.useState(false);
|
|
44
68
|
const [actionBusy, setActionBusy] = React.useState(false);
|
|
45
69
|
const [catalogCommand, setCatalogCommand] = React.useState(null);
|
|
46
|
-
const [
|
|
70
|
+
const [importCompletions, setImportCompletions] = React.useState({ CONTROLLED_CATALOG: null, SANCTION_CATALOG: null });
|
|
47
71
|
const [importFailures, setImportFailures] = React.useState({ CONTROLLED_CATALOG: null, SANCTION_CATALOG: null });
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
72
|
+
const catalogTaskTrackerRef = React.useRef(createPollingTaskTracker());
|
|
73
|
+
const contractTaskTrackerRef = React.useRef(createPollingTaskTracker());
|
|
74
|
+
const reanalysisTaskTrackerRef = React.useRef(createPollingTaskTracker());
|
|
75
|
+
const handledContractTasksRef = React.useRef(new Set());
|
|
76
|
+
const handledReanalysisResultsRef = React.useRef(new Set());
|
|
77
|
+
const pendingReviewResultsRef = React.useRef(createPendingResultQueue(item => item.kind === 'CONTRACT_IMPORT' ? item.completion.task.id : item.result.run.id, comparePendingReviewBusinessOrder));
|
|
78
|
+
const activeReviewResultRef = React.useRef(null);
|
|
79
|
+
const presentNextReviewRef = React.useRef(() => undefined);
|
|
80
|
+
const reviewPresentationTimerRef = React.useRef(null);
|
|
51
81
|
const blobUrlOwnerRef = React.useRef(createBlobUrlOwner());
|
|
52
82
|
const changeRouteRef = React.useRef(() => undefined);
|
|
53
83
|
const [procurementContext, setProcurementContext] = React.useState({ supplier: null, order: null, metrics: null });
|
|
@@ -57,15 +87,34 @@ function App() {
|
|
|
57
87
|
const [customerReanalysisState, setCustomerReanalysisState] = React.useState({ status: 'IDLE' });
|
|
58
88
|
const [activeSalesFileOrder, setActiveSalesFileOrder] = React.useState(null);
|
|
59
89
|
const [editingSalesFile, setEditingSalesFile] = React.useState(null);
|
|
60
|
-
const [
|
|
90
|
+
const [toasts, setToasts] = React.useState([]);
|
|
91
|
+
const [toastTimers] = React.useState(() => createToastTimerRegistry((id) => setToasts((items) => removeToast(items, id))));
|
|
61
92
|
const [analyticsError, setAnalyticsError] = React.useState(null);
|
|
93
|
+
const setToast = React.useCallback((toast) => {
|
|
94
|
+
setToasts((items) => addToast(items, toast, createToastId));
|
|
95
|
+
}, []);
|
|
96
|
+
const closeToast = React.useCallback((id) => {
|
|
97
|
+
toastTimers.dismiss(id);
|
|
98
|
+
setToasts((items) => removeToast(items, id));
|
|
99
|
+
}, [toastTimers]);
|
|
100
|
+
const configureAnalyticsPrototypeCapability = React.useCallback((parameters) => {
|
|
101
|
+
analyticsPrototypeListenerCleanupRef.current?.();
|
|
102
|
+
analyticsPrototypeListenerCleanupRef.current = null;
|
|
103
|
+
setAnalyticsPrototypeVisible(false);
|
|
104
|
+
if (parameters.visualCheck !== true)
|
|
105
|
+
return;
|
|
106
|
+
const showPrototypeBoard = () => setAnalyticsPrototypeVisible(true);
|
|
107
|
+
window.addEventListener('analytics-prototype-board', showPrototypeBoard);
|
|
108
|
+
analyticsPrototypeListenerCleanupRef.current = () => window.removeEventListener('analytics-prototype-board', showPrototypeBoard);
|
|
109
|
+
}, []);
|
|
62
110
|
const loadRoute = React.useCallback(async (targetRoute, page = 1, pageSize = 20, filters = {}) => {
|
|
63
|
-
const
|
|
111
|
+
const requestedPageSize = resolvePaginationPageSize(pageSize);
|
|
112
|
+
const request = routeDataRequest(targetRoute, page, requestedPageSize, filters);
|
|
64
113
|
if (!request)
|
|
65
114
|
return;
|
|
66
115
|
const collection = String(asRecord(request.parameters).collection);
|
|
67
116
|
const requests = collection === 'analytics' ? analyticsRequestsRef.current : routeRequestGateRef.current;
|
|
68
|
-
const analyticsCandidate = collection === 'analytics' ? analyticsQueryFromRoute(page,
|
|
117
|
+
const analyticsCandidate = collection === 'analytics' ? analyticsQueryFromRoute(page, requestedPageSize, filters) : null;
|
|
69
118
|
const requestId = analyticsCandidate ? analyticsRequestsRef.current.begin(analyticsCandidate) : routeRequestGateRef.current.begin();
|
|
70
119
|
setBusy(true);
|
|
71
120
|
try {
|
|
@@ -78,15 +127,18 @@ function App() {
|
|
|
78
127
|
setAnalyticsCommittedQuery(analyticsRequestsRef.current.committed());
|
|
79
128
|
setData(current => ({ ...current, analytics: normalizeWorkbenchData({ summary: { analytics: payload } }).analytics }));
|
|
80
129
|
}
|
|
81
|
-
else if (collection === 'settings')
|
|
82
|
-
|
|
130
|
+
else if (collection === 'settings') {
|
|
131
|
+
const settings = normalizeWorkbenchData({ summary: { settings: payload } }).settings;
|
|
132
|
+
settingsRatesQueryRef.current = { page: settings.rates.page, pageSize: settings.rates.pageSize };
|
|
133
|
+
setData(current => ({ ...current, settings }));
|
|
134
|
+
}
|
|
83
135
|
else
|
|
84
136
|
setData(current => ({
|
|
85
137
|
...current,
|
|
86
138
|
[collection]: {
|
|
87
139
|
...payload,
|
|
88
140
|
items: Array.isArray(payload.items) ? payload.items : [],
|
|
89
|
-
total: numberValue(payload.total, 0), page: numberValue(payload.page, page), pageSize:
|
|
141
|
+
total: numberValue(payload.total, 0), page: numberValue(payload.page, page), pageSize: requestedPageSize
|
|
90
142
|
}
|
|
91
143
|
}));
|
|
92
144
|
if (targetRoute.type === 'procurement')
|
|
@@ -116,7 +168,7 @@ function App() {
|
|
|
116
168
|
const requestId = gate.begin();
|
|
117
169
|
setBusy(true);
|
|
118
170
|
try {
|
|
119
|
-
const response = await bridge.requestData({ page: 1, pageSize:
|
|
171
|
+
const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
|
|
120
172
|
if (gate.isLatest(requestId)) {
|
|
121
173
|
const normalized = normalizeWorkbenchData(response);
|
|
122
174
|
bridge.configure({ uploadPolicy: normalized.uploadPolicy });
|
|
@@ -133,9 +185,68 @@ function App() {
|
|
|
133
185
|
setBusy(gate.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0);
|
|
134
186
|
}
|
|
135
187
|
}, [loadRoute]);
|
|
188
|
+
const loadTaskState = React.useCallback(async () => {
|
|
189
|
+
const active = contextRef.current;
|
|
190
|
+
if (!active)
|
|
191
|
+
return false;
|
|
192
|
+
const gate = taskStateRequestGateRef.current;
|
|
193
|
+
const requestId = gate.begin();
|
|
194
|
+
try {
|
|
195
|
+
const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
|
|
196
|
+
if (!gate.commit(requestId))
|
|
197
|
+
return false;
|
|
198
|
+
const normalized = normalizeWorkbenchData(response);
|
|
199
|
+
setData(current => ({
|
|
200
|
+
...current,
|
|
201
|
+
controlledGoods: normalized.controlledGoods,
|
|
202
|
+
controlledBatches: normalized.controlledBatches,
|
|
203
|
+
sanctionedCompanies: normalized.sanctionedCompanies,
|
|
204
|
+
sanctionBatches: normalized.sanctionBatches,
|
|
205
|
+
importTasks: normalized.importTasks,
|
|
206
|
+
customerContractAnalyses: normalized.customerContractAnalyses
|
|
207
|
+
}));
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (gate.isLatest(requestId))
|
|
212
|
+
setToast({ message: error instanceof Error ? `任务状态刷新失败:${error.message}` : '任务状态刷新失败', tone: 'error' });
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
gate.finish(requestId);
|
|
217
|
+
}
|
|
218
|
+
}, []);
|
|
219
|
+
const scheduleTaskStateRecovery = React.useCallback(() => {
|
|
220
|
+
if (taskRecoveryTimerRef.current != null)
|
|
221
|
+
return;
|
|
222
|
+
taskRecoveryTimerRef.current = window.setInterval(() => {
|
|
223
|
+
void loadTaskState().then(refreshed => {
|
|
224
|
+
if (!refreshed || taskRecoveryTimerRef.current == null)
|
|
225
|
+
return;
|
|
226
|
+
window.clearInterval(taskRecoveryTimerRef.current);
|
|
227
|
+
taskRecoveryTimerRef.current = null;
|
|
228
|
+
});
|
|
229
|
+
}, 2000);
|
|
230
|
+
}, [loadTaskState]);
|
|
231
|
+
React.useEffect(() => () => {
|
|
232
|
+
if (taskRecoveryTimerRef.current != null)
|
|
233
|
+
window.clearInterval(taskRecoveryTimerRef.current);
|
|
234
|
+
if (reviewPresentationTimerRef.current != null)
|
|
235
|
+
window.clearTimeout(reviewPresentationTimerRef.current);
|
|
236
|
+
}, []);
|
|
237
|
+
React.useEffect(() => {
|
|
238
|
+
return () => catalogTaskTrackerRef.current.abortAll();
|
|
239
|
+
}, []);
|
|
240
|
+
React.useEffect(() => {
|
|
241
|
+
return () => contractTaskTrackerRef.current.abortAll();
|
|
242
|
+
}, []);
|
|
243
|
+
React.useEffect(() => {
|
|
244
|
+
return () => reanalysisTaskTrackerRef.current.abortAll();
|
|
245
|
+
}, []);
|
|
136
246
|
React.useEffect(() => {
|
|
137
247
|
bridge.start({
|
|
138
248
|
onInit(nextContext) {
|
|
249
|
+
configureAnalyticsPrototypeCapability(asRecord(nextContext.initialQuery?.parameters));
|
|
139
250
|
contextRef.current = nextContext;
|
|
140
251
|
const normalized = normalizeWorkbenchData(nextContext.payload);
|
|
141
252
|
bridge.configure({ uploadPolicy: normalized.uploadPolicy });
|
|
@@ -146,86 +257,94 @@ function App() {
|
|
|
146
257
|
void load();
|
|
147
258
|
}
|
|
148
259
|
});
|
|
149
|
-
return () => {
|
|
150
|
-
|
|
260
|
+
return () => {
|
|
261
|
+
analyticsPrototypeListenerCleanupRef.current?.();
|
|
262
|
+
analyticsPrototypeListenerCleanupRef.current = null;
|
|
263
|
+
blobUrlOwnerRef.current.dispose();
|
|
264
|
+
bridge.destroy();
|
|
265
|
+
};
|
|
266
|
+
}, [configureAnalyticsPrototypeCapability, load]);
|
|
267
|
+
React.useEffect(() => () => toastTimers.dispose(), [toastTimers]);
|
|
151
268
|
React.useEffect(() => {
|
|
152
269
|
const timer = window.setTimeout(() => bridge.reportResize(), 0);
|
|
153
270
|
return () => window.clearTimeout(timer);
|
|
154
271
|
}, [route, data, busy]);
|
|
155
272
|
React.useEffect(() => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const timer = window.setTimeout(() => setToast(null), 4200);
|
|
159
|
-
return () => window.clearTimeout(timer);
|
|
160
|
-
}, [toast]);
|
|
273
|
+
toastTimers.sync(toasts);
|
|
274
|
+
}, [toastTimers, toasts]);
|
|
161
275
|
const executeAction = React.useCallback(async (actionKey, input) => {
|
|
162
276
|
setActionBusy(true);
|
|
163
277
|
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;
|
|
278
|
+
return await executeWorkbenchAction({
|
|
279
|
+
actionKey,
|
|
280
|
+
mutate: async () => asActionResult(unwrapResponse(await bridge.executeAction(actionKey, input))),
|
|
281
|
+
refresh: load,
|
|
282
|
+
notify: setToast,
|
|
283
|
+
resolveMessage: resolveActionMessage,
|
|
284
|
+
onResult: result => { if (actionKey === 'download_import_issues' || actionKey === 'download_sales_file')
|
|
285
|
+
downloadActionFile(result.data); }
|
|
286
|
+
});
|
|
177
287
|
}
|
|
178
288
|
finally {
|
|
179
289
|
setActionBusy(false);
|
|
180
290
|
}
|
|
181
291
|
}, [load]);
|
|
182
292
|
const executeSalesFileAction = React.useCallback((actionKey, input) => executeAction(actionKey, input), [executeAction]);
|
|
183
|
-
const
|
|
293
|
+
const schedulePendingReviewPresentation = React.useCallback(() => {
|
|
294
|
+
if (reviewPresentationTimerRef.current != null)
|
|
295
|
+
return;
|
|
296
|
+
reviewPresentationTimerRef.current = window.setTimeout(() => {
|
|
297
|
+
reviewPresentationTimerRef.current = null;
|
|
298
|
+
presentNextReviewRef.current();
|
|
299
|
+
}, 0);
|
|
300
|
+
}, []);
|
|
301
|
+
const enqueuePendingReview = React.useCallback((result) => {
|
|
302
|
+
pendingReviewResultsRef.current.enqueue(result);
|
|
303
|
+
schedulePendingReviewPresentation();
|
|
304
|
+
}, [schedulePendingReviewPresentation]);
|
|
305
|
+
const orchestrateImport = React.useCallback(async (start, signal) => {
|
|
184
306
|
const taskId = start.data?.task.id;
|
|
185
307
|
if (!taskId)
|
|
186
308
|
throw new Error('解析响应缺少任务标识');
|
|
187
|
-
if (runningImportTaskRef.current === taskId)
|
|
188
|
-
throw new Error('该目录任务正在解析中');
|
|
189
|
-
runningImportTaskRef.current = taskId;
|
|
190
|
-
setActionBusy(true);
|
|
191
309
|
const documentType = start.data?.task.documentType;
|
|
192
310
|
if (documentType)
|
|
193
311
|
setImportFailures((current) => ({ ...current, [documentType]: null }));
|
|
194
312
|
try {
|
|
195
313
|
const completion = await runCatalogImportOrchestration({
|
|
196
314
|
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
315
|
pollTask: async (taskId) => asImportCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
|
|
204
|
-
refresh:
|
|
316
|
+
refresh: loadTaskState,
|
|
317
|
+
signal,
|
|
318
|
+
onTransition: (previous, next) => {
|
|
319
|
+
const notice = taskTransition(previous, next);
|
|
320
|
+
if (notice)
|
|
321
|
+
setToast(notice);
|
|
322
|
+
},
|
|
323
|
+
onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
|
|
205
324
|
});
|
|
206
|
-
|
|
207
|
-
setToast({ message: '目录导入完成', tone: 'success' });
|
|
325
|
+
setImportCompletions(current => ({ ...current, [completion.task.documentType]: completion }));
|
|
208
326
|
return completion;
|
|
209
327
|
}
|
|
210
328
|
catch (error) {
|
|
211
|
-
if (error
|
|
329
|
+
if (isAbortError(error))
|
|
330
|
+
throw error;
|
|
331
|
+
if (isPollingObservationTimeout(error)) {
|
|
332
|
+
setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
|
|
333
|
+
scheduleTaskStateRecovery();
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
if (error instanceof CatalogImportError) {
|
|
212
337
|
setImportFailures((current) => ({ ...current, [error.task.documentType]: error.task }));
|
|
213
|
-
|
|
338
|
+
}
|
|
339
|
+
else
|
|
340
|
+
setToast({ message: error instanceof Error ? error.message : '目录解析失败', tone: 'error' });
|
|
214
341
|
throw error;
|
|
215
342
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
setActionBusy(false);
|
|
219
|
-
}
|
|
220
|
-
}, [load]);
|
|
221
|
-
const orchestrateContractImport = React.useCallback(async (start) => {
|
|
343
|
+
}, [loadTaskState, scheduleTaskStateRecovery]);
|
|
344
|
+
const orchestrateContractImport = React.useCallback(async (start, signal) => {
|
|
222
345
|
const task = start.data?.task;
|
|
223
346
|
if (!task)
|
|
224
347
|
throw new Error('解析响应缺少合同任务标识');
|
|
225
|
-
if (runningContractTasksRef.current.has(task.id))
|
|
226
|
-
throw new Error('该合同任务正在解析中');
|
|
227
|
-
runningContractTasksRef.current.add(task.id);
|
|
228
|
-
setActionBusy(true);
|
|
229
348
|
if (task.documentType === 'PURCHASE_CONTRACT') {
|
|
230
349
|
blobUrlOwnerRef.current.clear('purchase');
|
|
231
350
|
setPurchaseImportTask(task);
|
|
@@ -238,29 +357,28 @@ function App() {
|
|
|
238
357
|
try {
|
|
239
358
|
const completion = await runContractImportOrchestration({
|
|
240
359
|
start,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
360
|
+
signal,
|
|
361
|
+
pollTask: async (taskId) => asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
|
|
362
|
+
onTransition: (previous, next) => {
|
|
363
|
+
const notice = taskTransition(previous, next);
|
|
364
|
+
if (notice)
|
|
365
|
+
setToast(notice);
|
|
246
366
|
},
|
|
247
|
-
|
|
367
|
+
onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
|
|
248
368
|
});
|
|
249
|
-
handledContractTasksRef.current[task.documentType] = task.id;
|
|
250
369
|
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
|
-
}
|
|
370
|
+
enqueuePendingReview({ kind: 'CONTRACT_IMPORT', completion });
|
|
260
371
|
}
|
|
261
372
|
return completion;
|
|
262
373
|
}
|
|
263
374
|
catch (error) {
|
|
375
|
+
if (isAbortError(error))
|
|
376
|
+
throw error;
|
|
377
|
+
if (isPollingObservationTimeout(error)) {
|
|
378
|
+
setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
|
|
379
|
+
scheduleTaskStateRecovery();
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
264
382
|
const failed = error instanceof ContractImportError ? error.task : task;
|
|
265
383
|
if (task.documentType === 'PURCHASE_CONTRACT') {
|
|
266
384
|
setPurchaseImportTask({ ...failed, status: failed.status === 'TIMED_OUT' ? 'TIMED_OUT' : 'FAILED', errorMessage: error instanceof Error ? error.message : '采购合同解析失败' });
|
|
@@ -268,62 +386,58 @@ function App() {
|
|
|
268
386
|
}
|
|
269
387
|
else
|
|
270
388
|
setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: error instanceof Error ? error.message : '客户合同解析失败' });
|
|
271
|
-
|
|
389
|
+
if (!(error instanceof ContractImportError))
|
|
390
|
+
setToast({ message: error instanceof Error ? error.message : '合同解析失败', tone: 'error' });
|
|
272
391
|
throw error;
|
|
273
392
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
setActionBusy(false);
|
|
277
|
-
}
|
|
278
|
-
}, []);
|
|
279
|
-
const executeFileAction = React.useCallback(async (actionKey, file, input) => {
|
|
393
|
+
}, [enqueuePendingReview, scheduleTaskStateRecovery]);
|
|
394
|
+
const executeFileAction = React.useCallback(async (actionKey, file, input, notifyError = true) => {
|
|
280
395
|
try {
|
|
281
|
-
const
|
|
396
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeFileAction(actionKey, file, input), loadTaskState);
|
|
397
|
+
const start = asActionResult(unwrapResponse(raw));
|
|
282
398
|
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 };
|
|
399
|
+
throw actionResponseError(start, '上传失败');
|
|
400
|
+
setToast({ message: resolveActionMessage(start) || '解析任务已提交', tone: 'info' });
|
|
401
|
+
return start;
|
|
290
402
|
}
|
|
291
403
|
catch (error) {
|
|
292
|
-
if (!(error instanceof CatalogImportError))
|
|
404
|
+
if (notifyError && !(error instanceof CatalogImportError))
|
|
293
405
|
setToast({ message: error instanceof Error ? error.message : '上传失败', tone: 'error' });
|
|
294
406
|
throw error;
|
|
295
407
|
}
|
|
296
|
-
}, [
|
|
408
|
+
}, [loadTaskState]);
|
|
409
|
+
const executeCatalogFileAction = React.useCallback((actionKey, file, input) => executeFileAction(actionKey, file, input, false), [executeFileAction]);
|
|
297
410
|
React.useEffect(() => {
|
|
298
411
|
const tasks = data.importTasks.items;
|
|
299
412
|
setImportFailures({
|
|
300
413
|
CONTROLLED_CATALOG: findLatestFailedCatalogImportTask(tasks, 'CONTROLLED_CATALOG'),
|
|
301
414
|
SANCTION_CATALOG: findLatestFailedCatalogImportTask(tasks, 'SANCTION_CATALOG')
|
|
302
415
|
});
|
|
303
|
-
const active
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
416
|
+
for (const active of findActiveCatalogImportTasks(tasks)) {
|
|
417
|
+
const running = catalogTaskTrackerRef.current.run(active.id, signal => orchestrateImport({ success: true, data: { task: active } }, signal));
|
|
418
|
+
if (running)
|
|
419
|
+
void running.catch(() => undefined);
|
|
307
420
|
}
|
|
308
421
|
}, [data.importTasks.items, orchestrateImport]);
|
|
309
422
|
React.useEffect(() => {
|
|
310
423
|
const tasks = data.importTasks.items;
|
|
424
|
+
for (const resumable of findResumableContractImports(tasks)) {
|
|
425
|
+
if (handledContractTasksRef.current.has(resumable.id))
|
|
426
|
+
continue;
|
|
427
|
+
const running = contractTaskTrackerRef.current.run(resumable.id, signal => orchestrateContractImport({ success: true, data: { task: resumable } }, signal));
|
|
428
|
+
if (running)
|
|
429
|
+
void running.catch(() => undefined);
|
|
430
|
+
}
|
|
311
431
|
for (const documentType of ['PURCHASE_CONTRACT', 'CUSTOMER_CONTRACT']) {
|
|
312
|
-
const resumable = findResumableContractImport(tasks, documentType);
|
|
313
432
|
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) {
|
|
433
|
+
const hasResumable = tasks.some(task => task.documentType === documentType && (task.status === 'AWAITING_REVIEW' || ['QUEUED', 'EXTRACTING', 'AI_PARSING', 'VALIDATING', 'PERSISTING'].includes(task.status)));
|
|
434
|
+
if (!hasResumable && failed) {
|
|
321
435
|
if (documentType === 'PURCHASE_CONTRACT')
|
|
322
436
|
setPurchaseImportTask(failed);
|
|
323
437
|
else
|
|
324
438
|
setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: failed.errorMessage || '客户合同解析失败,请重新解析' });
|
|
325
439
|
}
|
|
326
|
-
if (!
|
|
440
|
+
if (!hasResumable && !failed && tasks.some(item => item.documentType === documentType && item.status === 'SUCCEEDED')) {
|
|
327
441
|
if (documentType === 'PURCHASE_CONTRACT') {
|
|
328
442
|
setPurchaseImportTask(null);
|
|
329
443
|
setPurchaseReviewDraft(null);
|
|
@@ -335,65 +449,173 @@ function App() {
|
|
|
335
449
|
}, [data.importTasks.items, orchestrateContractImport]);
|
|
336
450
|
const retryImport = React.useCallback(async (taskId) => {
|
|
337
451
|
try {
|
|
338
|
-
const
|
|
452
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
|
|
453
|
+
const start = asActionResult(unwrapResponse(raw));
|
|
339
454
|
if (start.success === false)
|
|
340
|
-
throw
|
|
341
|
-
|
|
455
|
+
throw actionResponseError(start, '重新解析失败');
|
|
456
|
+
setToast({ message: resolveActionMessage(start) || '重新解析任务已提交', tone: 'info' });
|
|
342
457
|
}
|
|
343
458
|
catch (error) {
|
|
344
459
|
if (!(error instanceof CatalogImportError))
|
|
345
460
|
setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
|
|
346
461
|
throw error;
|
|
347
462
|
}
|
|
348
|
-
}, [
|
|
463
|
+
}, [loadTaskState]);
|
|
349
464
|
const retryContractImport = React.useCallback(async (taskId) => {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
465
|
+
try {
|
|
466
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
|
|
467
|
+
const start = asActionResult(unwrapResponse(raw));
|
|
468
|
+
if (start.success === false)
|
|
469
|
+
throw actionResponseError(start, '重新解析失败');
|
|
470
|
+
setToast({ message: resolveActionMessage(start) || '重新解析任务已提交', tone: 'info' });
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
}, [loadTaskState]);
|
|
477
|
+
const applyCustomerContractReanalysisResult = React.useCallback(async (result) => {
|
|
478
|
+
let review;
|
|
479
|
+
if (result.review) {
|
|
480
|
+
review = adaptCustomerContractReview(result.review, { reviewSource: 'EXISTING_CONTRACT' });
|
|
481
|
+
}
|
|
482
|
+
else if (result.task) {
|
|
483
|
+
review = adaptCustomerContractReview(result.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: result.task.id, sourceFileId: result.task.sourceFileId ?? '', sourceFileName: result.task.sourceFileName ?? '' });
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
throw new Error('重新分析结果缺少合同审核数据');
|
|
487
|
+
}
|
|
488
|
+
const attached = await attachCustomerSource(review, blobUrlOwnerRef.current);
|
|
489
|
+
setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: result.task?.id ?? result.run.importTaskId ?? '', review: attached });
|
|
490
|
+
changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
|
|
491
|
+
return attached;
|
|
492
|
+
}, []);
|
|
493
|
+
const consumeCustomerContractReanalysisResult = React.useCallback(async (runId) => {
|
|
494
|
+
const consumed = asActionResult(unwrapResponse(await bridge.executeAction('consume_customer_contract_reanalysis_result', { runId })));
|
|
495
|
+
if (consumed.success === false)
|
|
496
|
+
throw new Error(resolveActionMessage(consumed) || '确认重新分析结果失败');
|
|
497
|
+
}, []);
|
|
358
498
|
const reanalyzeCustomerContract = React.useCallback(async (input) => {
|
|
359
499
|
setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
|
|
360
500
|
try {
|
|
361
|
-
const
|
|
501
|
+
const raw = await submitAndRefreshTaskState(() => bridge.executeAction('reanalyze_customer_contract', { reviewSource: input.reviewSource, ...(input.customerContractId ? { id: input.customerContractId } : {}), ...(input.importTaskId ? { importTaskId: input.importTaskId } : {}) }), loadTaskState);
|
|
502
|
+
const response = asActionResult(unwrapResponse(raw));
|
|
362
503
|
if (response.success === false || !response.data)
|
|
363
|
-
throw
|
|
364
|
-
|
|
504
|
+
throw actionResponseError(response, 'AI 重新分析启动失败');
|
|
505
|
+
setToast({ message: resolveActionMessage(response) || 'AI 重新分析任务已提交', tone: 'info' });
|
|
506
|
+
const running = reanalysisTaskTrackerRef.current.run(response.data.run.id, signal => runCustomerContractReanalysis({
|
|
365
507
|
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
|
-
}
|
|
508
|
+
signal,
|
|
509
|
+
poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } }))),
|
|
510
|
+
onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
|
|
511
|
+
}));
|
|
512
|
+
if (!running)
|
|
513
|
+
throw new Error('该 AI 重新分析任务正在跟踪中');
|
|
514
|
+
const result = await running;
|
|
515
|
+
const review = await applyCustomerContractReanalysisResult(result);
|
|
516
|
+
await consumeCustomerContractReanalysisResult(result.run.id);
|
|
517
|
+
handledReanalysisResultsRef.current.add(result.run.id);
|
|
389
518
|
setCustomerReanalysisState({ status: 'IDLE' });
|
|
519
|
+
setToast({ message: 'AI 重新分析完成', tone: 'success' });
|
|
520
|
+
await loadTaskState();
|
|
390
521
|
return review;
|
|
391
522
|
}
|
|
392
523
|
catch (error) {
|
|
524
|
+
if (isAbortError(error))
|
|
525
|
+
throw error;
|
|
526
|
+
if (error instanceof ActionResponseError && error.code === 'IMPORT_QUEUE_UNAVAILABLE') {
|
|
527
|
+
setCustomerReanalysisState({ status: 'RUNNING', message: '任务已保存,等待队列服务恢复' });
|
|
528
|
+
setToast({ message: error.message, tone: 'info' });
|
|
529
|
+
scheduleTaskStateRecovery();
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
if (isPollingObservationTimeout(error)) {
|
|
533
|
+
setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析仍在后台处理中' });
|
|
534
|
+
setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
|
|
535
|
+
scheduleTaskStateRecovery();
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
393
538
|
setCustomerReanalysisState({ status: 'FAILED', message: error instanceof Error ? error.message : 'AI 重新分析失败' });
|
|
539
|
+
setToast({ message: error instanceof Error ? error.message : 'AI 重新分析失败', tone: 'error' });
|
|
394
540
|
throw error;
|
|
395
541
|
}
|
|
396
|
-
}, []);
|
|
542
|
+
}, [applyCustomerContractReanalysisResult, consumeCustomerContractReanalysisResult, loadTaskState, scheduleTaskStateRecovery]);
|
|
543
|
+
React.useEffect(() => {
|
|
544
|
+
for (const recovery of data.customerContractAnalyses.items) {
|
|
545
|
+
const recoverable = recovery.recoveryType === 'ACTIVE_POLL' || recovery.recoveryType === 'SUCCEEDED_RESULT';
|
|
546
|
+
if (!recoverable)
|
|
547
|
+
continue;
|
|
548
|
+
if (handledReanalysisResultsRef.current.has(recovery.run.id))
|
|
549
|
+
continue;
|
|
550
|
+
const running = reanalysisTaskTrackerRef.current.run(recovery.run.id, signal => runCustomerContractReanalysis({
|
|
551
|
+
start: { run: recovery.run },
|
|
552
|
+
signal,
|
|
553
|
+
poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } }))),
|
|
554
|
+
onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
|
|
555
|
+
}));
|
|
556
|
+
if (!running)
|
|
557
|
+
continue;
|
|
558
|
+
setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
|
|
559
|
+
setToast({ message: '已恢复 AI 重新分析任务', tone: 'info' });
|
|
560
|
+
void running.then(result => {
|
|
561
|
+
enqueuePendingReview({ kind: 'REANALYSIS', result });
|
|
562
|
+
}).catch(error => {
|
|
563
|
+
if (isAbortError(error))
|
|
564
|
+
return;
|
|
565
|
+
if (isPollingObservationTimeout(error)) {
|
|
566
|
+
setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析仍在后台处理中' });
|
|
567
|
+
setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
|
|
568
|
+
scheduleTaskStateRecovery();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
setCustomerReanalysisState({ status: 'FAILED', message: error instanceof Error ? error.message : 'AI 重新分析失败' });
|
|
572
|
+
setToast({ message: error instanceof Error ? error.message : 'AI 重新分析失败', tone: 'error' });
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}, [data.customerContractAnalyses.items, enqueuePendingReview, scheduleTaskStateRecovery]);
|
|
576
|
+
const presentNextPendingReview = React.useCallback(async () => {
|
|
577
|
+
if (activeReviewResultRef.current)
|
|
578
|
+
return;
|
|
579
|
+
const pending = pendingReviewResultsRef.current.peek();
|
|
580
|
+
if (!pending)
|
|
581
|
+
return;
|
|
582
|
+
const resultId = pending.kind === 'CONTRACT_IMPORT' ? pending.completion.task.id : pending.result.run.id;
|
|
583
|
+
activeReviewResultRef.current = resultId;
|
|
584
|
+
try {
|
|
585
|
+
if (pending.kind === 'CONTRACT_IMPORT') {
|
|
586
|
+
const task = pending.completion.task;
|
|
587
|
+
if (task.documentType === 'PURCHASE_CONTRACT') {
|
|
588
|
+
blobUrlOwnerRef.current.clear('purchase');
|
|
589
|
+
const draft = adaptPurchaseReview(task);
|
|
590
|
+
setPurchaseImportTask(task);
|
|
591
|
+
setPurchaseReviewDraft(await attachPurchaseSource(draft, task, blobUrlOwnerRef.current));
|
|
592
|
+
changeRouteRef.current({ type: 'procurement', level: 'suppliers' });
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
blobUrlOwnerRef.current.clear('customer');
|
|
596
|
+
const review = adaptCustomerContractReview(task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: task.id, sourceFileId: task.sourceFileId ?? '', sourceFileName: task.sourceFileName ?? '' });
|
|
597
|
+
setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: task.id, review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
|
|
598
|
+
changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
|
|
599
|
+
}
|
|
600
|
+
pendingReviewResultsRef.current.ack(resultId);
|
|
601
|
+
handledContractTasksRef.current.add(resultId);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
await applyCustomerContractReanalysisResult(pending.result);
|
|
605
|
+
await consumeCustomerContractReanalysisResult(pending.result.run.id);
|
|
606
|
+
pendingReviewResultsRef.current.ack(resultId);
|
|
607
|
+
handledReanalysisResultsRef.current.add(pending.result.run.id);
|
|
608
|
+
setCustomerReanalysisState({ status: 'IDLE' });
|
|
609
|
+
setToast({ message: 'AI 重新分析完成', tone: 'success' });
|
|
610
|
+
await loadTaskState();
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
activeReviewResultRef.current = null;
|
|
614
|
+
setToast({ message: error instanceof Error ? error.message : '审核结果恢复失败', tone: 'error' });
|
|
615
|
+
scheduleTaskStateRecovery();
|
|
616
|
+
}
|
|
617
|
+
}, [applyCustomerContractReanalysisResult, consumeCustomerContractReanalysisResult, loadTaskState, scheduleTaskStateRecovery]);
|
|
618
|
+
presentNextReviewRef.current = () => { void presentNextPendingReview(); };
|
|
397
619
|
const loadCustomerContractReview = React.useCallback(async (customerContractId) => {
|
|
398
620
|
const result = asActionResult(unwrapResponse(await bridge.executeAction('get_customer_contract_review', { id: customerContractId })));
|
|
399
621
|
if (result.success === false || !result.data)
|
|
@@ -405,21 +627,27 @@ function App() {
|
|
|
405
627
|
const closePurchaseReview = React.useCallback(() => {
|
|
406
628
|
blobUrlOwnerRef.current.clear('purchase');
|
|
407
629
|
setPurchaseReviewDraft(null);
|
|
408
|
-
|
|
630
|
+
activeReviewResultRef.current = null;
|
|
631
|
+
schedulePendingReviewPresentation();
|
|
632
|
+
}, [schedulePendingReviewPresentation]);
|
|
409
633
|
const closeCustomerReview = React.useCallback(() => {
|
|
410
634
|
blobUrlOwnerRef.current.clear('customer');
|
|
411
635
|
setCustomerImportState({ status: 'IDLE' });
|
|
412
|
-
|
|
636
|
+
activeReviewResultRef.current = null;
|
|
637
|
+
schedulePendingReviewPresentation();
|
|
638
|
+
}, [schedulePendingReviewPresentation]);
|
|
413
639
|
const queryImportIssues = React.useCallback(async (query) => {
|
|
414
|
-
const
|
|
415
|
-
|
|
640
|
+
const requestedPageSize = resolvePaginationPageSize(query.pageSize);
|
|
641
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection: 'importIssues', documentType: query.documentType, batchId: query.batchId, issueType: query.issueType } })));
|
|
642
|
+
return { items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: requestedPageSize };
|
|
416
643
|
}, []);
|
|
417
644
|
const queryCatalog = React.useCallback(async (collection, query) => {
|
|
645
|
+
const requestedPageSize = resolvePaginationPageSize(query.pageSize);
|
|
418
646
|
const gate = requestGateRef.current;
|
|
419
647
|
const requestId = gate.begin();
|
|
420
648
|
setBusy(true);
|
|
421
649
|
try {
|
|
422
|
-
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize:
|
|
650
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection, filters: encodeViewFilters(query.filters) } })));
|
|
423
651
|
if (!gate.isLatest(requestId))
|
|
424
652
|
return;
|
|
425
653
|
setData((current) => ({
|
|
@@ -429,7 +657,7 @@ function App() {
|
|
|
429
657
|
items: Array.isArray(payload.items) ? payload.items : [],
|
|
430
658
|
total: numberValue(payload.total, 0),
|
|
431
659
|
page: numberValue(payload.page, query.page),
|
|
432
|
-
pageSize:
|
|
660
|
+
pageSize: requestedPageSize
|
|
433
661
|
}
|
|
434
662
|
}));
|
|
435
663
|
}
|
|
@@ -442,6 +670,27 @@ function App() {
|
|
|
442
670
|
setBusy(gate.activeCount() > 0);
|
|
443
671
|
}
|
|
444
672
|
}, []);
|
|
673
|
+
const queryMonthlyRates = React.useCallback(async (query) => {
|
|
674
|
+
const requestedPageSize = resolvePaginationPageSize(query.pageSize);
|
|
675
|
+
const gate = settingsRatesRequestGateRef.current;
|
|
676
|
+
const requestId = gate.begin();
|
|
677
|
+
setBusy(true);
|
|
678
|
+
try {
|
|
679
|
+
const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection: 'monthlyRates' } })));
|
|
680
|
+
if (!gate.commit(requestId))
|
|
681
|
+
return;
|
|
682
|
+
settingsRatesQueryRef.current = { page: numberValue(payload.page, query.page), pageSize: requestedPageSize };
|
|
683
|
+
setData(current => ({ ...current, settings: normalizeWorkbenchData({ summary: { settings: { ...current.settings, rates: { ...payload, pageSize: requestedPageSize } } } }).settings }));
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
if (gate.isLatest(requestId))
|
|
687
|
+
setToast({ message: error instanceof Error ? `月度汇率加载失败:${error.message}` : '月度汇率加载失败', tone: 'error' });
|
|
688
|
+
}
|
|
689
|
+
finally {
|
|
690
|
+
gate.finish(requestId);
|
|
691
|
+
setBusy(gate.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() > 0);
|
|
692
|
+
}
|
|
693
|
+
}, []);
|
|
445
694
|
const queryProcurement = React.useCallback(async (_collection, query) => {
|
|
446
695
|
await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
|
|
447
696
|
}, [loadRoute]);
|
|
@@ -452,20 +701,36 @@ function App() {
|
|
|
452
701
|
await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
|
|
453
702
|
}, [loadRoute]);
|
|
454
703
|
const querySalesFiles = React.useCallback(async (collection, query) => {
|
|
704
|
+
const requestedPageSize = resolvePaginationPageSize(query.pageSize);
|
|
455
705
|
if (collection === 'salesFileOrders') {
|
|
456
|
-
await loadRoute({ type: 'page', page: 'sales-files' }, query.page,
|
|
706
|
+
await loadRoute({ type: 'page', page: 'sales-files' }, query.page, requestedPageSize, query.filters);
|
|
457
707
|
return;
|
|
458
708
|
}
|
|
459
|
-
const
|
|
460
|
-
|
|
709
|
+
const key = generatedFileHistoryKey(query.parameters);
|
|
710
|
+
if (!key) {
|
|
711
|
+
setToast({ message: '历史生成文件查询缺少合同或采购订单标识', tone: 'error' });
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
activeGeneratedFilesKeyRef.current = key;
|
|
715
|
+
await runKeyedLatestRequest({
|
|
716
|
+
gate: generatedFilesRequestGateRef.current,
|
|
717
|
+
key,
|
|
718
|
+
request: async () => asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection, ...query.parameters, filters: encodeViewFilters(query.filters) } }))),
|
|
719
|
+
isCurrent: () => activeGeneratedFilesKeyRef.current === key,
|
|
720
|
+
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 } })),
|
|
721
|
+
onError: error => setToast({ message: error instanceof Error ? `历史生成文件加载失败:${error.message}` : '历史生成文件加载失败', tone: 'error' }),
|
|
722
|
+
onStart: () => setBusy(true),
|
|
723
|
+
onFinish: () => setBusy(generatedFilesRequestGateRef.current.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0)
|
|
724
|
+
});
|
|
461
725
|
}, [loadRoute]);
|
|
462
726
|
const queryAnalytics = React.useCallback(async (query) => {
|
|
727
|
+
const requestedQuery = { ...query, pageSize: resolvePaginationPageSize(query.pageSize) };
|
|
463
728
|
const requests = analyticsRequestsRef.current;
|
|
464
|
-
const requestId = requests.begin(
|
|
729
|
+
const requestId = requests.begin(requestedQuery);
|
|
465
730
|
setBusy(true);
|
|
466
731
|
setAnalyticsError(null);
|
|
467
732
|
try {
|
|
468
|
-
const payload = asRecord(unwrapResponse(await bridge.requestData({ page:
|
|
733
|
+
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
734
|
if (!requests.commit(requestId))
|
|
470
735
|
return;
|
|
471
736
|
setAnalyticsCommittedQuery(requests.committed());
|
|
@@ -509,25 +774,26 @@ function App() {
|
|
|
509
774
|
const result = asActionResult(unwrapResponse(await bridge.executeAction('save_settings', request)));
|
|
510
775
|
if (result.success === false)
|
|
511
776
|
throw new Error(resolveActionMessage(result) || '设置保存失败');
|
|
512
|
-
await loadRoute({ type: 'page', page: 'settings' });
|
|
777
|
+
await loadRoute({ type: 'page', page: 'settings' }, settingsRatesQueryRef.current.page, settingsRatesQueryRef.current.pageSize);
|
|
513
778
|
}, [loadRoute]);
|
|
514
779
|
const restoreSettings = React.useCallback(async (request) => {
|
|
515
780
|
const result = asActionResult(unwrapResponse(await bridge.executeAction('restore_settings_default', request)));
|
|
516
781
|
if (result.success === false)
|
|
517
782
|
throw new Error(resolveActionMessage(result) || '恢复系统默认失败');
|
|
518
|
-
await loadRoute({ type: 'page', page: 'settings' });
|
|
783
|
+
await loadRoute({ type: 'page', page: 'settings' }, settingsRatesQueryRef.current.page, settingsRatesQueryRef.current.pageSize);
|
|
519
784
|
}, [loadRoute]);
|
|
520
785
|
const deleteSettingsRate = React.useCallback(async (request) => {
|
|
521
786
|
const result = asActionResult(unwrapResponse(await bridge.executeAction('delete_settings_rate', request)));
|
|
522
787
|
if (result.success === false)
|
|
523
788
|
throw new Error(resolveActionMessage(result) || '月度汇率删除失败');
|
|
524
|
-
|
|
525
|
-
|
|
789
|
+
const { page, pageSize } = settingsRatesQueryRef.current;
|
|
790
|
+
const clampedPage = clampPageAfterSingleDeletion(page, pageSize, data.settings.rates.total);
|
|
791
|
+
await loadRoute({ type: 'page', page: 'settings' }, clampedPage, pageSize);
|
|
792
|
+
}, [data.settings.rates.total, loadRoute]);
|
|
526
793
|
const uploadCompanyLogo = React.useCallback(async (file) => {
|
|
527
794
|
const result = asActionResult(unwrapResponse(await bridge.executeFileAction('upload_company_logo', file, {})));
|
|
528
795
|
if (result.success === false || !result.data?.logoFileId)
|
|
529
796
|
throw new Error(resolveActionMessage(result) || '公司 Logo 上传失败');
|
|
530
|
-
setToast({ message: '公司 Logo 上传成功', tone: 'success' });
|
|
531
797
|
return result.data.logoFileId;
|
|
532
798
|
}, []);
|
|
533
799
|
const discardCompanyLogo = React.useCallback(async (logoFileId) => {
|
|
@@ -537,8 +803,22 @@ function App() {
|
|
|
537
803
|
}, []);
|
|
538
804
|
const openSalesFiles = React.useCallback((order) => {
|
|
539
805
|
setActiveSalesFileOrder(order);
|
|
540
|
-
if (order)
|
|
541
|
-
|
|
806
|
+
if (!order)
|
|
807
|
+
return;
|
|
808
|
+
changeRouteRef.current({ type: 'page', page: 'sales-files' });
|
|
809
|
+
void querySalesFiles('generatedFiles', {
|
|
810
|
+
page: 1,
|
|
811
|
+
pageSize: 20,
|
|
812
|
+
filters: {},
|
|
813
|
+
parameters: { customerContractId: order.customerContractId, purchaseOrderId: order.purchaseOrderId }
|
|
814
|
+
});
|
|
815
|
+
}, [querySalesFiles]);
|
|
816
|
+
const selectSalesFileOrder = React.useCallback((order) => {
|
|
817
|
+
const previousKey = activeGeneratedFilesKeyRef.current;
|
|
818
|
+
if (previousKey)
|
|
819
|
+
generatedFilesRequestGateRef.current.invalidate(previousKey);
|
|
820
|
+
activeGeneratedFilesKeyRef.current = order ? generatedFileHistoryKey(order) : null;
|
|
821
|
+
setActiveSalesFileOrder(order);
|
|
542
822
|
}, []);
|
|
543
823
|
const changeRoute = React.useCallback((nextRoute) => {
|
|
544
824
|
routeRef.current = nextRoute;
|
|
@@ -548,12 +828,16 @@ function App() {
|
|
|
548
828
|
changeRouteRef.current = changeRoute;
|
|
549
829
|
const page = routePage(route);
|
|
550
830
|
const meta = pageMeta[page];
|
|
551
|
-
const contractCommands = page === 'procurement'
|
|
552
|
-
?
|
|
831
|
+
const contractCommands = page === 'procurement' && route.type === 'procurement'
|
|
832
|
+
? route.level === 'orders'
|
|
833
|
+
? _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", disabled: busy || actionBusy, onClick: () => changeRoute({ type: 'procurement', level: 'suppliers' }), children: [_jsx("i", { className: "ri-add-line", "aria-hidden": "true" }), "\u65B0\u5EFA\u91C7\u8D2D\u8BA2\u5355"] })
|
|
834
|
+
: _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u91C7\u8D2D\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_purchase_contract', file, { documentType: 'PURCHASE_CONTRACT' }) })
|
|
553
835
|
: page === 'customer-contract-review'
|
|
554
836
|
? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u5BA2\u6237\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_customer_contract', file, { documentType: 'CUSTOMER_CONTRACT' }) })
|
|
555
837
|
: undefined;
|
|
556
|
-
|
|
838
|
+
if (analyticsPrototypeVisible)
|
|
839
|
+
return (_jsx(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, onRouteChange: changeRoute, onRefresh: () => void load(), children: _jsx(AnalyticsPrototypeBoard, { onClose: () => setAnalyticsPrototypeVisible(false) }) }));
|
|
840
|
+
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, 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
841
|
void querySalesFiles('generatedFiles', { page: 1, pageSize: 20, filters: {}, parameters: { customerContractId: activeSalesFileOrder.customerContractId, purchaseOrderId: activeSalesFileOrder.purchaseOrderId } }); } })] }));
|
|
558
842
|
}
|
|
559
843
|
function routePage(route) {
|
|
@@ -564,6 +848,7 @@ function routePage(route) {
|
|
|
564
848
|
return route.page;
|
|
565
849
|
}
|
|
566
850
|
function analyticsQueryFromRoute(page, pageSize, values) {
|
|
851
|
+
const requestedPageSize = resolvePaginationPageSize(pageSize);
|
|
567
852
|
const dateValue = values.orderDate;
|
|
568
853
|
const date = dateValue && typeof dateValue === 'object' && !Array.isArray(dateValue) ? dateValue : null;
|
|
569
854
|
const orderDate = date ? { from: optionalText(date.from), to: optionalText(date.to) } : undefined;
|
|
@@ -574,11 +859,11 @@ function analyticsQueryFromRoute(page, pageSize, values) {
|
|
|
574
859
|
};
|
|
575
860
|
if (values.mode === 'SALES')
|
|
576
861
|
return {
|
|
577
|
-
mode: 'SALES', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'CUSTOMER', page, pageSize,
|
|
862
|
+
mode: 'SALES', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'CUSTOMER', page, pageSize: requestedPageSize,
|
|
578
863
|
filters: { ...common, ...(optionalText(values.customer) ? { customer: optionalText(values.customer) } : {}), ...(enabled(values.unreceivedOnly) ? { unreceivedOnly: true } : {}) }
|
|
579
864
|
};
|
|
580
865
|
return {
|
|
581
|
-
mode: 'PURCHASE', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'SUPPLIER', page, pageSize,
|
|
866
|
+
mode: 'PURCHASE', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'SUPPLIER', page, pageSize: requestedPageSize,
|
|
582
867
|
filters: { ...common, ...(optionalText(values.supplier) ? { supplier: optionalText(values.supplier) } : {}), ...(enabled(values.unpaidOnly) ? { unpaidOnly: true } : {}), ...(enabled(values.unrefundedOnly) ? { unrefundedOnly: true } : {}) }
|
|
583
868
|
};
|
|
584
869
|
}
|
|
@@ -596,6 +881,9 @@ function resolveActionMessage(result) {
|
|
|
596
881
|
}
|
|
597
882
|
return '';
|
|
598
883
|
}
|
|
884
|
+
function actionResponseError(result, fallback) {
|
|
885
|
+
return new ActionResponseError(resolveActionMessage(result) || fallback, result.error?.code);
|
|
886
|
+
}
|
|
599
887
|
function asRecord(value) {
|
|
600
888
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
601
889
|
}
|
|
@@ -694,9 +982,6 @@ function asCustomerContractReanalysisResult(value) {
|
|
|
694
982
|
throw new Error('轮询响应缺少重新分析任务');
|
|
695
983
|
return payload;
|
|
696
984
|
}
|
|
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
985
|
async function ensureExcelEditorRuntime() {
|
|
701
986
|
const runtimeWindow = window;
|
|
702
987
|
if (runtimeWindow.TradeComplianceExcelEditor?.open)
|
|
@@ -737,7 +1022,7 @@ async function ensureExcelEditorRuntime() {
|
|
|
737
1022
|
});
|
|
738
1023
|
return excelEditorRuntimePromise;
|
|
739
1024
|
}
|
|
740
|
-
function SalesFileEditor({ file, onClose, onSaved }) {
|
|
1025
|
+
function SalesFileEditor({ file, onClose, onSaved, onNotify }) {
|
|
741
1026
|
const containerRef = React.useRef(null);
|
|
742
1027
|
const editorRef = React.useRef(null);
|
|
743
1028
|
const [error, setError] = React.useState('');
|
|
@@ -778,12 +1063,13 @@ function SalesFileEditor({ file, onClose, onSaved }) {
|
|
|
778
1063
|
return () => { cancelled = true; editorRef.current?.dispose(); editorRef.current = null; };
|
|
779
1064
|
}, [file.id]);
|
|
780
1065
|
const save = async () => { if (!editorRef.current)
|
|
781
|
-
return; setSaving(true);
|
|
1066
|
+
return; setSaving(true); try {
|
|
782
1067
|
await editorRef.current.save();
|
|
1068
|
+
onNotify({ message: '工作簿保存成功', tone: 'success' });
|
|
783
1069
|
onSaved();
|
|
784
1070
|
}
|
|
785
1071
|
catch (cause) {
|
|
786
|
-
|
|
1072
|
+
onNotify({ message: cause instanceof Error ? cause.message : '工作簿保存失败', tone: 'error' });
|
|
787
1073
|
}
|
|
788
1074
|
finally {
|
|
789
1075
|
setSaving(false);
|