fix(aihr): harden broadcast and mobile assistant flows

This commit is contained in:
2026-07-23 03:35:36 +08:00
parent 62c8b41672
commit 4b31848e3d
30 changed files with 1127 additions and 208 deletions
+4 -2
View File
@@ -145,9 +145,11 @@ export const getPracticeReviews = () =>
timeout: 15000
});
export const getPracticeTeamSnapshot = () =>
export const getPracticeTeamSnapshot = (includeProfiles = false) =>
apiRequest<PracticeTeamSnapshot>({
url: '/api/aihr/mobile/practice/team',
url: includeProfiles
? '/api/aihr/mobile/practice/team?includeProfiles=true'
: '/api/aihr/mobile/practice/team',
method: 'GET',
timeout: 15000
});
+78 -14
View File
@@ -2,7 +2,8 @@ import type {
PracticeAlert,
PracticeAssignment,
PracticeRecord,
PracticeTeamMember
PracticeTeamMember,
PracticeTeamProfileSummary
} from '@/types/api';
export type SupervisorMetricKey = 'pendingReviews' | 'pendingAssignments' | 'lowScores' | 'alerts';
@@ -30,6 +31,7 @@ export interface TeamProfile {
latestScoreText: string;
latestScene: string;
lastPracticeAt: string;
completedToday?: boolean;
pendingReview: number;
pendingAssignment: number;
reason: string;
@@ -101,6 +103,9 @@ interface TeamProfileDraft {
export const manualPracticeAssignments = (assignments: PracticeAssignment[]) =>
assignments.filter((item) => item.source !== 'daily');
const normalizedTeamCount = (value: number | null | undefined) =>
typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
export const actionablePracticeAlerts = (alerts: PracticeAlert[]) =>
alerts.filter((item) => item.type !== 'stable');
@@ -186,9 +191,20 @@ export const buildSupervisorMetrics = (
export const buildTeamProfiles = (
reviews: PracticeRecord[],
assignments: PracticeAssignment[],
members: PracticeTeamMember[] = []
members: PracticeTeamMember[] = [],
profileSummaries: PracticeTeamProfileSummary[] = []
): TeamProfile[] => {
const rows = new Map<string, TeamProfileDraft>();
const summariesByExtPartyId = new Map(
profileSummaries
.filter((item) => Boolean(item?.extPartyId?.trim()))
.map((item) => [item.extPartyId.trim(), item] as const)
);
const hasFullTeamSummaries = summariesByExtPartyId.size > 0;
const memberExtPartyIds = new Set(
members.map((item) => item.extPartyId.trim()).filter(Boolean)
);
const normalizedExtPartyId = (value?: string | null) => value?.trim() || '';
const ensure = (rawKey: string | undefined, rawName: string, rawExtPartyId?: string) => {
const extPartyId = rawExtPartyId?.trim() || '';
const candidateName = rawName.trim();
@@ -224,6 +240,7 @@ export const buildTeamProfiles = (
});
reviews.forEach((item) => {
if (hasFullTeamSummaries && !memberExtPartyIds.has(normalizedExtPartyId(item.extPartyId))) return;
const row = ensure(item.extPartyId || item.trainee, item.trainee, item.extPartyId);
if (isReviewEditable(item.status)) {
row.pendingReview += 1;
@@ -245,6 +262,7 @@ export const buildTeamProfiles = (
});
manualPracticeAssignments(assignments).forEach((item) => {
if (hasFullTeamSummaries && !memberExtPartyIds.has(normalizedExtPartyId(item.extPartyId))) return;
const row = ensure(item.extPartyId || item.traineeName, item.traineeName, item.extPartyId);
if (item.status !== '已完成') row.pendingAssignment += 1;
row.latestScene ||= item.scenarioName.trim();
@@ -258,19 +276,31 @@ export const buildTeamProfiles = (
return Array.from(rows.values())
.map<TeamProfile>((row) => {
const latestScore = row.latestPractice?.score;
const latestScene = row.latestPractice?.scene || row.latestScene;
const risk: TeamProfile['risk'] = row.pendingLowScore !== undefined
const summary = summariesByExtPartyId.get(row.extPartyId);
const latestScore = typeof summary?.latestScore === 'number' && Number.isFinite(summary.latestScore)
? summary.latestScore
: row.latestPractice?.score;
const latestScene = summary?.latestScene?.trim() || row.latestPractice?.scene || row.latestScene;
const lastPracticeAt = summary?.lastPracticeAt?.trim() || row.latestPractice?.time || '';
const completedToday = typeof summary?.completedToday === 'boolean'
? summary.completedToday
: undefined;
const pendingReview = summary ? normalizedTeamCount(summary.pendingReview) : row.pendingReview;
const pendingAssignment = summary ? normalizedTeamCount(summary.pendingAssignment) : row.pendingAssignment;
const pendingLowScore = summary && typeof summary.pendingLowScore === 'number' && Number.isFinite(summary.pendingLowScore)
? summary.pendingLowScore
: row.pendingLowScore;
const risk: TeamProfile['risk'] = pendingLowScore !== undefined
? 'high'
: row.pendingReview > 0
: pendingReview > 0
? 'medium'
: 'low';
const reason = risk === 'high'
? `${row.name} 有未复盘低分记录(${row.pendingLowScore}分),建议定向再练`
? `${row.name} 有未复盘低分记录(${pendingLowScore}分),建议定向再练`
: risk === 'medium'
? `${row.name} 有 ${row.pendingReview} 条待复盘,建议完成点评后派专项`
: row.pendingAssignment > 0
? `${row.name} 有 ${row.pendingAssignment} 项待训练,建议跟进完成`
? `${row.name} 有 ${pendingReview} 条待复盘,建议完成点评后派专项`
: pendingAssignment > 0
? `${row.name} 有 ${pendingAssignment} 项待训练,建议跟进完成`
: `${row.name} 当前无待处理训练,可按需派发专项`;
return {
@@ -283,9 +313,10 @@ export const buildTeamProfiles = (
latestScore,
latestScoreText: latestScore === undefined ? '暂无评分' : `${latestScore}分`,
latestScene,
lastPracticeAt: row.latestPractice?.time || '',
pendingReview: row.pendingReview,
pendingAssignment: row.pendingAssignment,
lastPracticeAt,
completedToday,
pendingReview,
pendingAssignment,
reason,
risk
};
@@ -297,6 +328,35 @@ export const buildTeamProfiles = (
|| (a.latestScore ?? 100) - (b.latestScore ?? 100));
};
export interface TeamProfileRollup {
teamTotal: number;
pendingReview: number;
pendingAssignment: number;
todayDone: number;
}
/**
* Profiles are the only complete team-level source. Recent activity lists are
* intentionally capped, so dashboard counts must not derive from them.
*/
export const buildTeamProfileRollup = (
profiles: TeamProfile[],
todayKey: string
): TeamProfileRollup => profiles.reduce<TeamProfileRollup>((rollup, profile) => {
rollup.teamTotal += 1;
rollup.pendingReview += normalizedTeamCount(profile.pendingReview);
rollup.pendingAssignment += normalizedTeamCount(profile.pendingAssignment);
const completedToday = profile.completedToday === true
|| (profile.completedToday === undefined && Boolean(todayKey) && profile.lastPracticeAt.startsWith(todayKey));
if (completedToday) rollup.todayDone += 1;
return rollup;
}, {
teamTotal: 0,
pendingReview: 0,
pendingAssignment: 0,
todayDone: 0
});
export const filterTeamProfiles = <T extends Pick<TeamProfile, 'displayName' | 'extPartyId'>>(
profiles: T[],
query: string
@@ -313,7 +373,11 @@ export const paginateTeamProfiles = <T>(profiles: T[], requestedPage: number, pa
const pageCount = Math.max(1, Math.ceil(profiles.length / safeSize));
const page = Math.min(pageCount, Math.max(1, Math.floor(requestedPage)));
return {
items: profiles.slice((page - 1) * safeSize, page * safeSize),
// The team page uses this as a progressive “load more” view, not a
// replacement pager. Keep earlier members visible when the next batch is
// requested so the control matches its label and does not make the list
// jump backward.
items: profiles.slice(0, page * safeSize),
page,
pageCount
};