import { useCallback, useEffect, useMemo, useState } from 'react'; import { App, Alert, Button, Empty, Input, Modal, Space, Spin, Table, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { UserOutlined } from '@ant-design/icons'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { getTaskScoreDetail, saveTaskUserScore } from '@/api/appraisal'; import PageBackButton from '@/components/PageBackButton'; import { usePermission } from '@/contexts/PermissionContext'; import '@/styles/permission-link.css'; import './appraisal-detail.css'; interface DetailItem { _rowKey?: string; id?: string | number; reviewCategory?: string; reviewItem?: string; remarks?: string; score?: number; weight?: number; remark?: string; [key: string]: unknown; } interface GroupRow { category: string; items: DetailItem[]; remarkCate: string; } interface ExamineTask { taskName?: string; templateId?: string | number; templateType?: string | number; } interface ExamineUser { id?: string | number; userName?: string; manageScore?: string | number; judgeContent?: string; selfJudgeContent?: string; examineStatus?: string | number; examineStatusSelf?: string | number; } interface RemarkRow { remark?: string; } const isObject = (value: unknown): value is Record => typeof value === 'object' && value !== null; const toNumber = (value: unknown, fallback = 0) => { const num = Number(value); return Number.isFinite(num) ? num : fallback; }; const clampScore = (value: number) => Math.min(10, Math.max(0, Math.round(value))); const toBoolean = (value: unknown) => { if (typeof value === 'boolean') { return value; } if (typeof value === 'number') { return value === 1; } const raw = String(value ?? '').toLowerCase().trim(); return raw === '1' || raw === 'true' || raw === 'yes'; }; const normalizePayload = (response: unknown) => isObject(response) && response.data !== undefined ? response.data : response; const groupDetailRows = (items: DetailItem[], remarks: RemarkRow[] = []) => { const map = new Map(); items.forEach((item) => { const key = String(item.reviewCategory ?? '未分类'); const list = map.get(key) ?? []; const fallbackKey = `${key}_${list.length}`; list.push({ ...item, _rowKey: String(item.id ?? fallbackKey), score: toNumber(item.score, 0), weight: toNumber(item.weight, 0), remark: String(item.remark ?? ''), }); map.set(key, list); }); const groups: GroupRow[] = []; Array.from(map.entries()).forEach(([category, list], index) => { groups.push({ category, items: list, remarkCate: String(remarks[index]?.remark ?? ''), }); }); return groups; }; interface ScoreBarProps { value: number; editable: boolean; onChange: (value: number) => void; } const ScoreBar = ({ value, editable, onChange }: ScoreBarProps) => { const normalized = clampScore(value); const updateByPercent = (percent: number) => { onChange(clampScore(percent * 10)); }; const handleTrackClick = (event: React.MouseEvent) => { if (!editable) { return; } const rect = event.currentTarget.getBoundingClientRect(); if (rect.width <= 0) { return; } const percent = (event.clientX - rect.left) / rect.width; updateByPercent(percent); }; const handleKeyDown = (event: React.KeyboardEvent) => { if (!editable) { return; } if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') { event.preventDefault(); onChange(clampScore(normalized - 1)); return; } if (event.key === 'ArrowRight' || event.key === 'ArrowUp') { event.preventDefault(); onChange(clampScore(normalized + 1)); } }; const bubblePosition = normalized * 10; const bubbleClass = normalized === 0 ? 'detail-score-bubble is-start' : normalized === 10 ? 'detail-score-bubble is-end' : 'detail-score-bubble'; return (
0
{normalized}
{normalized === 0 &&
暂未打分
}
); }; const AppraisalDetailPage = () => { const { message, modal } = App.useApp(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { canAccessPath } = usePermission(); const isNormal = toBoolean(searchParams.get('isNormal')); const [isEdit, setIsEdit] = useState(toBoolean(searchParams.get('edit'))); const examineTaskId = searchParams.get('examineTaskId') ?? searchParams.get('taskId') ?? ''; const reviewType = searchParams.get('reviewType') ?? (isNormal ? '1' : '0'); const routeExamineId = searchParams.get('examineId') ?? ''; const routeUserId = searchParams.get('userId') ?? ''; const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); const [examineTask, setExamineTask] = useState({}); const [examineUser, setExamineUser] = useState({}); const [groups, setGroups] = useState([]); const [manageScore, setManageScore] = useState(0); const [judgeContent, setJudgeContent] = useState(''); const [remarkModalOpen, setRemarkModalOpen] = useState(false); const [editingCell, setEditingCell] = useState<{ groupIndex: number; rowIndex: number } | null>(null); const [remarkDraft, setRemarkDraft] = useState(''); const canEditScore = canAccessPath('/workAppraisal/detail'); const effectiveIsEdit = isEdit && canEditScore; const templateType = useMemo(() => String(examineTask.templateType ?? ''), [examineTask.templateType]); const isTemplateZero = templateType === '0'; const recalcManageScore = useCallback((nextGroups: GroupRow[]) => { const score = nextGroups .flatMap((group) => group.items) .reduce((sum, row) => sum + toNumber(row.score) * toNumber(row.weight), 0) / 10; setManageScore(Number(score.toFixed(2))); }, []); const loadDetail = useCallback(async () => { if (!examineTaskId) { return; } setLoading(true); try { const params: Record = { examineTaskId, reviewType, }; if (routeExamineId) { params.examineId = routeExamineId; } if (routeUserId) { params.userId = routeUserId; } const response = await getTaskScoreDetail(params); const payload = normalizePayload(response); if (!isObject(payload)) { message.error('评分详情返回格式异常'); setGroups([]); return; } const detailRows = Array.isArray(payload.examineConfigDetailVoList) ? (payload.examineConfigDetailVoList as DetailItem[]) : []; const remarks = Array.isArray(payload.remark) ? (payload.remark as RemarkRow[]) : []; const nextGroups = groupDetailRows(detailRows, remarks); setGroups(nextGroups); const task = isObject(payload.examineTask) ? (payload.examineTask as ExamineTask) : {}; const user = isObject(payload.examineUser) ? (payload.examineUser as ExamineUser) : {}; setExamineTask(task); setExamineUser(user); if (isNormal && String(task.templateType ?? '') === '0') { setJudgeContent(String(user.selfJudgeContent ?? user.judgeContent ?? '')); } else { setJudgeContent(String(user.judgeContent ?? '')); } const currentManageScore = toNumber(user.manageScore); if (currentManageScore > 0) { setManageScore(currentManageScore); } else if (!isNormal) { recalcManageScore(nextGroups); } if (String(user.examineStatusSelf ?? '') === '1' && String(user.examineStatus ?? '') === '1') { setIsEdit(false); } } catch (error) { console.error('Failed to fetch score detail:', error); message.error('获取评分详情失败'); setGroups([]); } finally { setLoading(false); } }, [examineTaskId, isNormal, recalcManageScore, reviewType, routeExamineId, routeUserId]); useEffect(() => { void loadDetail(); }, [loadDetail]); const updateItemScore = (groupIndex: number, rowIndex: number, value: number) => { setGroups((prev) => { const next = prev.map((group, gIndex) => { if (gIndex !== groupIndex) { return group; } return { ...group, items: group.items.map((row, rIndex) => (rIndex === rowIndex ? { ...row, score: value } : row)), }; }); if (!isNormal) { recalcManageScore(next); } return next; }); }; const updateGroupRemark = (groupIndex: number, value: string) => { setGroups((prev) => prev.map((group, idx) => idx === groupIndex ? { ...group, remarkCate: value, } : group, ), ); }; const openRemarkModal = (groupIndex: number, rowIndex: number) => { const row = groups[groupIndex]?.items[rowIndex]; setEditingCell({ groupIndex, rowIndex }); setRemarkDraft(String(row?.remark ?? '')); setRemarkModalOpen(true); }; const saveRemarkModal = () => { if (!editingCell) { setRemarkModalOpen(false); return; } if (remarkDraft.length > 200) { message.warning('自评总结限制200个字符'); return; } setGroups((prev) => prev.map((group, gIndex) => { if (gIndex !== editingCell.groupIndex) { return group; } return { ...group, items: group.items.map((row, rIndex) => rIndex === editingCell.rowIndex ? { ...row, remark: remarkDraft, } : row, ), }; }), ); setRemarkModalOpen(false); }; const buildPayload = (submitStatus: 0 | 1) => { const allItems = groups.flatMap((group) => group.items); const detailList = allItems.map((item) => ({ score: toNumber(item.score, 0), configId: item.id, remark: String(item.remark ?? ''), reviewCategory: item.reviewCategory, })); const payload: Record = { examineId: examineUser.id ?? routeExamineId ?? '', taskId: examineTaskId, reviewType, examineDetailList: detailList, examineRemarkList: [], manageScore: manageScore, judgeContent, }; if (isNormal) { payload.examineStatusSelf = submitStatus; } else { payload.examineStatus = submitStatus; } if (isNormal && !isTemplateZero) { payload.examineRemarkList = groups .filter((group) => group.category !== '发展与协作') .map((group) => ({ reviewCategory: group.category, remark: group.remarkCate, })); } if (isNormal && isTemplateZero) { payload.selfJudgeContent = judgeContent; payload.judgeContent = ''; } return payload; }; const validateSubmit = (submitStatus: 0 | 1) => { if (submitStatus === 0) { return true; } const allItems = groups.flatMap((group) => group.items); const hasUnscored = allItems.some( (item) => !toNumber(item.score, 0) && (!isNormal || item.reviewCategory !== '发展与协作'), ); if (hasUnscored) { message.warning('存在未评分绩效项,请完善后再试'); return false; } if (isNormal) { const devRows = allItems.filter((item) => item.reviewCategory === '发展与协作'); const hasEmptyDevRemark = devRows.some((item) => !String(item.remark ?? '').trim()); if (hasEmptyDevRemark) { message.warning('发展与协作下的自评总结为必填,请完善后再试'); return false; } const hasShortDevRemark = devRows.some( (item) => String(item.remark ?? '').trim().length > 0 && String(item.remark ?? '').trim().length < 100, ); if (hasShortDevRemark) { message.warning('发展与协作下的自评总结最少100个字符,请完善后再试'); return false; } } if (!isNormal && isTemplateZero && judgeContent.length > 300) { message.warning('总体评价限制300个字符'); return false; } if (!isNormal && !judgeContent.trim()) { message.warning('总体评价为必填'); return false; } if (isNormal && isTemplateZero && !judgeContent.trim()) { message.warning('个人总体评价为必填'); return false; } if (isNormal && !isTemplateZero) { const cateRemarks = groups.filter((group) => group.category !== '发展与协作').map((group) => group.remarkCate); if (cateRemarks.some((remark) => !String(remark).trim())) { message.warning('存在未填写大类评价,请完善后再试'); return false; } if (cateRemarks.some((remark) => String(remark).trim().length < 100)) { message.warning('大类评价最少100个字符,请完善后再试'); return false; } } return true; }; const submitScore = async (submitStatus: 0 | 1) => { if (!effectiveIsEdit) { return; } if (!validateSubmit(submitStatus)) { return; } const doSubmit = async () => { setSubmitting(true); try { const payload = buildPayload(submitStatus); await saveTaskUserScore(payload); message.success('操作成功'); navigate(-1); } catch (error) { console.error('Failed to save score:', error); message.error('保存失败'); } finally { setSubmitting(false); } }; if (submitStatus === 1) { modal.confirm({ title: '确认提交绩效评分', content: '提交后将无法修改,该操作不可逆,请确认后再试', okText: '确定', cancelText: '取消', onOk: doSubmit, }); return; } await doSubmit(); }; const buildColumns = (group: GroupRow, groupIndex: number): ColumnsType => { const cols: ColumnsType = [ { title: '考核项', dataIndex: 'reviewItem', width: 200, }, { title: '评分标准', dataIndex: 'remarks', }, ]; if (!isNormal && examineTask.templateId && group.category === '发展与协作' && !isTemplateZero) { cols.push({ title: '员工自评', dataIndex: 'remark', render: (_, row) => , }); } if ((isNormal && group.category !== '发展与协作') || !isNormal) { cols.push({ title: '评分', dataIndex: 'score', width: 380, render: (_, row, rowIndex) => ( updateItemScore(groupIndex, rowIndex, nextScore)} /> ), }); } if (isNormal && ((examineTask.templateId && group.category === '发展与协作') || !examineTask.templateId)) { cols.push({ title: '自评总结', dataIndex: 'remark', width: 140, render: (_, row, rowIndex) => ( effectiveIsEdit ? ( ) : ( {String(row.remark ?? '').trim() ? '查看' : '暂未评价'} ) ), }); } return cols; }; return (
{!examineTaskId && }
{examineTask.taskName ?? '绩效考核详情'}
{effectiveIsEdit && ( )}
{examineUser.userName ?? '-'}
{!isNormal && (
考核评分: {manageScore}
)}
{groups.length === 0 ? ( ) : ( groups.map((group, groupIndex) => (
{group.category}
rowKey="_rowKey" columns={buildColumns(group, groupIndex)} dataSource={group.items} pagination={false} size="middle" /> {isNormal && !isTemplateZero && group.category !== '发展与协作' && (
评价
updateGroupRemark(groupIndex, event.target.value)} readOnly={!effectiveIsEdit} placeholder="0/300" />
)}
)) )} {((isNormal && isTemplateZero) || !isNormal) && (
总体评价
setJudgeContent(event.target.value)} readOnly={!effectiveIsEdit} placeholder="0/300" />
)}
setRemarkModalOpen(false)} onCancel={() => setRemarkModalOpen(false)} okButtonProps={{ style: { display: effectiveIsEdit ? 'inline-flex' : 'none' } }} okText="确定" cancelText={effectiveIsEdit ? '取消' : '关闭'} > setRemarkDraft(event.target.value)} readOnly={!effectiveIsEdit} placeholder="0/200" />
); }; export default AppraisalDetailPage;