diff --git a/app/performance-survey/[[...path]]/page.tsx b/app/performance-survey/[[...path]]/page.tsx index 7c4f221..ef76373 100644 --- a/app/performance-survey/[[...path]]/page.tsx +++ b/app/performance-survey/[[...path]]/page.tsx @@ -9,6 +9,7 @@ import { pinyin } from 'pinyin-pro' import { createPortal } from 'react-dom' import { sortPerformanceSurveyImagesByName } from '@/lib/performance-survey-image-order' import { calculatePerformanceSurveyResponseProgress } from '@/lib/performance-survey-response-progress' +import { groupVisualScoresByEvaluator } from '@/lib/performance-survey-result-grouping' type ApiState = { loading: boolean @@ -1691,6 +1692,103 @@ function ScoreParticipantList({ items }: { items: any[] }) { ) } +function VisualEvaluatorResultList({ + responses, + workItems, + questions, + template, +}: { + responses: any[] + workItems: any[] + questions: Template['questions'] + template?: Template +}) { + const evaluators = groupVisualScoresByEvaluator(responses, workItems) + + if (evaluators.length === 0) { + return
暂无评分记录
+ } + + return ( +
+ {evaluators.map((evaluator) => ( +
+
+
+
+ {evaluator.respondentName} + {['visual_final', 'self_visual_final'].includes(String(evaluator.workflowRole || '')) && ( + 计入最终分 + )} + {['self', 'self_visual_final'].includes(String(evaluator.workflowRole || '')) && ( + 自评 + )} + {evaluator.workflowRole === 'manager' && ( + 主管评分 + )} +
+ {evaluator.createdAt &&
提交于 {formatDate(evaluator.createdAt)}
} +
+
+
{formatResultScore(evaluator.totalScore)}
+
总分
+
+
+ +
+ {evaluator.workItemScores.map((item, itemIndex) => { + const itemScore = item.score + const rawScores = itemScore?.scores && typeof itemScore.scores === 'object' + ? itemScore.scores as Record + : {} + const dimensions = questions + .map((question) => ({ + question, + value: rawScores[question.id], + })) + .filter(({ value }) => value !== null && value !== undefined && value !== '') + + return ( +
+
+
+
作品 {itemIndex + 1}
+
{item.workItemTitle}
+
+
+ {itemScore ? `${formatResultScore(itemScore.totalScore)} 分` : '未评分'} +
+
+ {dimensions.length > 0 && ( +
+ {dimensions.map(({ question, value }) => ( +
+ {question.title} + + {formatResultScore(value)} / {getQuestionMaxScore(template, question)} + +
+ ))} +
+ )} + {itemScore?.comment &&

{itemScore.comment}

} +
+ ) + })} +
+ + {evaluator.comment && ( +
+
总体评语
+

{evaluator.comment}

+
+ )} +
+ ))} +
+ ) +} + function MetricCard({ label, value }: { label: string; value: ReactNode }) { return (
@@ -5170,6 +5268,7 @@ function SurveyResults({ surveyId }: { surveyId: string }) { {targets.map((target: any) => { const aggregate = target.aggregate || {} const workItems = Array.isArray(target.workItems) ? target.workItems : [] + const responses = Array.isArray(target.responses) ? target.responses : [] const dimensionEntries = questions .map((question: Template['questions'][number]) => ({ question, @@ -5250,47 +5349,39 @@ function SurveyResults({ surveyId }: { surveyId: string }) {
- ({ - ...score, - name: score.evaluatorName, - score: score.totalScore, - countsTowardFinal: ['visual_final', 'self_visual_final'].includes(score.workflowRole), - }))} - /> ))} )} -
-

{visualTemplate ? '全部评分记录' : '评价明细'}

-
-
- {target.responses.map((response: any) => ( -
-
- - {response.respondentName} - {visualTemplate && ['visual_final', 'self_visual_final'].includes(response.workflowRole) && ( - 计入最终分 - )} - {visualTemplate && ['self', 'self_visual_final'].includes(response.workflowRole) && ( - 自评 - )} - {visualTemplate && response.workflowRole === 'manager' && ( - 主管评分 - )} - - {formatResultScore(response.totalScore)} 分 +
+

{visualTemplate ? `评分人明细(${responses.length}人)` : '评价明细'}

+
+ {visualTemplate ? ( + + ) : ( +
+ {responses.map((response: any) => ( +
+
+ {response.respondentName} + {formatResultScore(response.totalScore)} 分 +
+ {response.comment &&

{response.comment}

} +
+ ))} + {responses.length === 0 && ( +
暂无评价
+ )}
- {response.comment &&

{response.comment}

} -
- ))} - {target.responses.length === 0 && ( -
暂无评价
- )} -
+ )} +
+ ) })} diff --git a/lib/__tests__/performance-survey-result-grouping.test.ts b/lib/__tests__/performance-survey-result-grouping.test.ts new file mode 100644 index 0000000..44315f4 --- /dev/null +++ b/lib/__tests__/performance-survey-result-grouping.test.ts @@ -0,0 +1,50 @@ +import { groupVisualScoresByEvaluator } from '@/lib/performance-survey-result-grouping' + +describe('performance survey visual result grouping', () => { + const workItems = [1, 2, 3].map((index) => ({ + id: `work-${index}`, + title: `作品 ${index}`, + scores: [ + { id: `score-a-${index}`, participantKey: 'participant-a', totalScore: index === 1 ? 0 : index * 10 }, + { id: `score-b-${index}`, participantKey: 'participant-b', totalScore: index * 10 + 1 }, + ], + })) + + it('三个作品只生成一次作品数据,并按评分人汇总各自三项评分', () => { + const result = groupVisualScoresByEvaluator([ + { id: 'response-a', participantKey: 'participant-a', respondentName: '评分人A' }, + { id: 'response-b', participantKey: 'participant-b', respondentName: '评分人B' }, + ], workItems) + + expect(result).toHaveLength(2) + expect(result[0].workItemScores).toHaveLength(3) + expect(result[0].workItemScores.map((item) => item.score?.id)).toEqual([ + 'score-a-1', + 'score-a-2', + 'score-a-3', + ]) + expect(result[1].workItemScores.map((item) => item.score?.id)).toEqual([ + 'score-b-1', + 'score-b-2', + 'score-b-3', + ]) + }) + + it('同名评分人依靠参与键区分,并保留合法的零分', () => { + const result = groupVisualScoresByEvaluator([ + { id: 'response-a', participantKey: 'participant-a', respondentName: '同名' }, + { id: 'response-b', participantKey: 'participant-b', respondentName: '同名' }, + ], workItems) + + expect(result[0].workItemScores[0].score?.totalScore).toBe(0) + expect(result[1].workItemScores[0].score?.totalScore).toBe(11) + }) + + it('缺失的作品评分保持为空,不伪造成零分', () => { + const result = groupVisualScoresByEvaluator([ + { id: 'response-a', participantKey: 'participant-a' }, + ], [{ id: 'work-missing', title: '未评分作品', scores: [] }]) + + expect(result[0].workItemScores[0].score).toBeNull() + }) +}) diff --git a/lib/performance-survey-result-grouping.ts b/lib/performance-survey-result-grouping.ts new file mode 100644 index 0000000..368a8a9 --- /dev/null +++ b/lib/performance-survey-result-grouping.ts @@ -0,0 +1,39 @@ +export type VisualEvaluatorResponse = { + id: string + participantKey?: string | null + respondentName?: string | null + totalScore?: unknown + comment?: string | null + workflowRole?: string | null + createdAt?: string | Date | null +} + +export type VisualEvaluatorWorkItem = { + id: string + title?: string | null + scores?: Array<{ + id: string + participantKey?: string | null + totalScore?: unknown + scores?: unknown + comment?: string | null + workflowRole?: string | null + createdAt?: string | Date | null + }> +} + +export function groupVisualScoresByEvaluator( + responses: VisualEvaluatorResponse[], + workItems: VisualEvaluatorWorkItem[], +) { + return responses.map((response) => ({ + ...response, + workItemScores: workItems.map((workItem, index) => ({ + workItemId: workItem.id, + workItemTitle: workItem.title || `作品 ${index + 1}`, + score: (workItem.scores || []).find( + (item) => Boolean(response.participantKey) && item.participantKey === response.participantKey, + ) || null, + })), + })) +} diff --git a/lib/performance-survey-service.ts b/lib/performance-survey-service.ts index 59a3eee..06c9b63 100644 --- a/lib/performance-survey-service.ts +++ b/lib/performance-survey-service.ts @@ -2631,6 +2631,15 @@ export async function getPerformanceSurveyResults( counts: survey._count, }, targets: survey.targets.map((target) => { + const participantByAssignmentId = new Map( + target.responses.map((response, index) => [ + response.assignmentId, + { + key: `participant-${index + 1}`, + name: showRealRespondent ? response.respondentName : `匿名评价 ${index + 1}`, + }, + ]), + ) const aggregateResponses = isVisualCaseTemplate(template) ? target.responses.filter((response) => isFinalWorkflowRole(readWorkflowRole(response.assignment.evaluatorSnapshot))) : target.responses @@ -2662,7 +2671,8 @@ export async function getPerformanceSurveyResults( aggregate: visibleAggregate, responses: target.responses.map((response, index) => ({ id: response.id, - respondentName: showRealRespondent ? response.respondentName : `匿名评价 ${index + 1}`, + participantKey: participantByAssignmentId.get(response.assignmentId)?.key || `participant-${index + 1}`, + respondentName: participantByAssignmentId.get(response.assignmentId)?.name || `匿名评价 ${index + 1}`, scores: response.scores, comment: response.comment, totalScore: response.totalScore, @@ -2674,17 +2684,21 @@ export async function getPerformanceSurveyResults( })), workItems: target.workItems.map((item) => { const images = serializePerformanceSurveyImagePayload(item.imageUrls) - const scores = item.scores.map((score, index) => ({ - id: score.id, - evaluatorName: showRealRespondent ? score.evaluatorName : `匿名评价 ${index + 1}`, - scores: score.scores, - comment: score.comment, - totalScore: score.totalScore, - workflowRole: readWorkflowRole( - target.responses.find((response) => response.assignmentId === score.assignmentId)?.assignment.evaluatorSnapshot, - ), - createdAt: score.createdAt, - })) + const scores = item.scores.map((score) => { + const participant = participantByAssignmentId.get(score.assignmentId) + return { + id: score.id, + participantKey: participant?.key || null, + evaluatorName: participant?.name || (showRealRespondent ? score.evaluatorName : '匿名评价'), + scores: score.scores, + comment: score.comment, + totalScore: score.totalScore, + workflowRole: readWorkflowRole( + target.responses.find((response) => response.assignmentId === score.assignmentId)?.assignment.evaluatorSnapshot, + ), + createdAt: score.createdAt, + } + }) const aggregateScores = isVisualCaseTemplate(template) ? scores.filter((score) => isFinalWorkflowRole(score.workflowRole)) : scores