Просмотр исходного кода

fix(s8): load order review pi config from s0

Validation: S8 pi-config request chain and fallback path verified; syncEnabled=true path was not covered because the external sync flag is not enabled.
YY968XX 3 месяцев назад
Родитель
Сommit
a53f8a45fe

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.187",
+	"version": "2.4.188",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 41 - 0
Web/src/views/aidop/s8/api/s8OrderReviewPiConfigApi.ts

@@ -0,0 +1,41 @@
+// S8-ORDER-REVIEW-PI-FROM-S0-1:订单评审 PI 标准配置 S0 只读消费 API。
+// 端点归属 S0(/api/s0/sales/contract-review-cycles/pi-config),但封装放在 S8 侧,
+// 避免与 S0 在途批次共改 s0SalesApi.ts;类型字段与后端
+// AdoS0ContractReviewCyclePiConfigDto / *StageDto / *BreakdownDto 一一对齐。
+import service from '/@/utils/request';
+
+function unwrap<T>(res: { data: T }): T {
+	return res.data;
+}
+
+export interface S0ContractReviewCyclePiStage {
+	stageCode: string;
+	stageName: string;
+	stdHours: number;
+	orderNo: number;
+}
+
+export interface S0ContractReviewCyclePiBreakdown {
+	groupCode: string;
+	groupName: string;
+	stdHours: number;
+	orderNo: number;
+}
+
+export interface S0ContractReviewCyclePiConfig {
+	factoryRefId: number;
+	syncEnabled: boolean;
+	mainStages: S0ContractReviewCyclePiStage[];
+	opinionFeedbackBreakdown: S0ContractReviewCyclePiBreakdown[];
+}
+
+export function getContractReviewCyclePiConfig(
+	factoryRefId: number,
+): Promise<S0ContractReviewCyclePiConfig> {
+	return service
+		.get<S0ContractReviewCyclePiConfig>(
+			'/api/s0/sales/contract-review-cycles/pi-config',
+			{ params: { factoryRefId } },
+		)
+		.then(unwrap);
+}

+ 26 - 3
Web/src/views/aidop/s8/monitoring/OrderChainOverviewPage.vue

@@ -3,7 +3,7 @@
 // ORDER-FLOW-CHAIN-FILTER-STAGE-1:6 维多选筛选 + 关键环节默认全选 + 全图矩阵 + 点击列切换详情焦点。
 // 入口:总览页订单卡「链路全景」按钮 → store.selectOrderForChain + router.push。
 // 刷新恢复:onMounted 调 store.restoreChainSelection() 从 sessionStorage 重建 selectedOrderNo。
-import { computed, onMounted, ref, watch } from 'vue';
+import { computed, onMounted, ref, shallowRef, watch } from 'vue';
 import { useRouter } from 'vue-router';
 import { useOrderExecutionStore } from '/@/stores/orderExecution';
 import type {
@@ -43,6 +43,15 @@ import {
 	PPT_REVIEW_SUBSTEPS,
 	PPT_OPINION_DRILLDOWN,
 } from '/@/views/aidop/s8/monitoring/data/order-execution/ppt-baseline';
+// S8-ORDER-REVIEW-PI-FROM-S0-1:baseline 态订单评审 PI 优先消费 S0 pi-config。
+import {
+	getContractReviewCyclePiConfig,
+	type S0ContractReviewCyclePiConfig,
+} from '/@/views/aidop/s8/api/s8OrderReviewPiConfigApi';
+import {
+	mergeReviewSubStepsWithS0Pi,
+	buildOpinionBreakdownFromS0Pi,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/pi-config-merge';
 
 const ALL_STAGE_KEYS: OrderNodeKey[] = [
 	'order_review',
@@ -60,10 +69,24 @@ const ORDER_REVIEW_STAGE_KEY: OrderNodeKey = 'order_review';
 const store = useOrderExecutionStore();
 const router = useRouter();
 
+// S8-ORDER-REVIEW-PI-FROM-S0-1:S0 PI 配置以 SysOrg 雪花 ID 索引,本批单工厂硬编码。
+const REVIEW_PI_FACTORY_REF_ID = 1329900200002; // S0-F001 一工厂 SysOrg Id
+const reviewPiConfig = shallowRef<S0ContractReviewCyclePiConfig | null>(null);
+
+async function loadReviewPiConfig() {
+	try {
+		reviewPiConfig.value = await getContractReviewCyclePiConfig(REVIEW_PI_FACTORY_REF_ID);
+	} catch (err) {
+		console.warn('[OrderChainOverview] pi-config fetch failed, fallback to PPT baseline', err);
+		reviewPiConfig.value = null;
+	}
+}
+
 onMounted(async () => {
 	// ORDER-FLOW-S8-INTEGRATED-DOMAIN-RESET-1 t3i:Chain 页走正式 API。
 	await store.loadFromDomain();
 	void store.loadAggregateFromDomain('BASELINE_PPT');
+	void loadReviewPiConfig();
 	store.restoreChainSelection();
 	const focused = store.selectedOrderNo;
 	if (focused) {
@@ -196,7 +219,7 @@ const currentStageSubSteps = computed<SubStepDetail[]>(() => {
 	const key = activeStageKey.value;
 	if (!key) return [];
 	if (isUnfiltered.value && key === ORDER_REVIEW_STAGE_KEY) {
-		return PPT_REVIEW_SUBSTEPS;
+		return mergeReviewSubStepsWithS0Pi(PPT_REVIEW_SUBSTEPS, reviewPiConfig.value, classifyStatus);
 	}
 	const ordersSubSteps = filteredChainOrders.value
 		.map((o) => o.lifecycle.find((s) => s.key === key)?.subSteps)
@@ -214,7 +237,7 @@ const currentStageBreakdown = computed<SubStepDetail[]>(() => {
 	const key = activeStageKey.value;
 	if (!key) return [];
 	if (isUnfiltered.value && key === ORDER_REVIEW_STAGE_KEY) {
-		return PPT_OPINION_DRILLDOWN;
+		return buildOpinionBreakdownFromS0Pi(PPT_OPINION_DRILLDOWN, reviewPiConfig.value, classifyStatus);
 	}
 	const ordersBreakdowns = filteredChainOrders.value
 		.map(

+ 66 - 0
Web/src/views/aidop/s8/monitoring/data/order-execution/pi-config-merge.ts

@@ -0,0 +1,66 @@
+// S8-ORDER-REVIEW-PI-FROM-S0-1:S0 pi-config → S8 baseline 注入工具。
+// 纯函数、无副作用,不依赖 Vue / service;不修改输入数组,fallback 返回浅克隆。
+import type { SubStepDetail } from './types';
+import type {
+	S0ContractReviewCyclePiConfig,
+	S0ContractReviewCyclePiStage,
+} from '/@/views/aidop/s8/api/s8OrderReviewPiConfigApi';
+
+type ClassifyStatusFn = (actual: number, pi: number) => SubStepDetail['status'];
+
+export function isUsablePiConfig(
+	config: S0ContractReviewCyclePiConfig | null,
+): config is S0ContractReviewCyclePiConfig {
+	return (
+		config !== null &&
+		config.syncEnabled === true &&
+		(config.mainStages.length > 0 || config.opinionFeedbackBreakdown.length > 0)
+	);
+}
+
+export function cloneSubSteps(items: readonly SubStepDetail[]): SubStepDetail[] {
+	return items.map((item) => ({ ...item }));
+}
+
+export function mergeReviewSubStepsWithS0Pi(
+	fallback: readonly SubStepDetail[],
+	config: S0ContractReviewCyclePiConfig | null,
+	classifyStatus: ClassifyStatusFn,
+): SubStepDetail[] {
+	if (!isUsablePiConfig(config) || config.mainStages.length === 0) {
+		return cloneSubSteps(fallback);
+	}
+	const byName = new Map<string, S0ContractReviewCyclePiStage>();
+	for (const stage of config.mainStages) {
+		byName.set(stage.stageName, stage);
+	}
+	return fallback.map((fb) => {
+		const hit = byName.get(fb.name);
+		if (!hit) return { ...fb };
+		const pi = Number(hit.stdHours);
+		const status: SubStepDetail['status'] =
+			fb.actualHours === null ? 'pending' : classifyStatus(fb.actualHours, pi);
+		return { ...fb, piHours: pi, status };
+	});
+}
+
+export function buildOpinionBreakdownFromS0Pi(
+	fallback: readonly SubStepDetail[],
+	config: S0ContractReviewCyclePiConfig | null,
+	classifyStatus: ClassifyStatusFn,
+): SubStepDetail[] {
+	if (!isUsablePiConfig(config) || config.opinionFeedbackBreakdown.length === 0) {
+		return cloneSubSteps(fallback);
+	}
+	return config.opinionFeedbackBreakdown.map((group, index) => {
+		const fbActual = fallback[index]?.actualHours ?? 0;
+		const pi = Number(group.stdHours);
+		const status = classifyStatus(fbActual, pi);
+		return {
+			name: group.groupName,
+			piHours: pi,
+			actualHours: fbActual,
+			status,
+		};
+	});
+}