fix(aihr): harden broadcast and mobile assistant flows
This commit is contained in:
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user