diff --git a/build_ui/src/views/train/checkin/attendancePolicy.js b/build_ui/src/views/train/checkin/attendancePolicy.js new file mode 100644 index 000000000..a0f051dee --- /dev/null +++ b/build_ui/src/views/train/checkin/attendancePolicy.js @@ -0,0 +1,146 @@ +/** + * 课次考勤与点名签到生产策略模块 (Attendance Policy Helper) + * 供 checkin/index.vue 生产页面与 Node.js 单元测试统一复用。 + * + * 核心原则: + * 1. 严格区分 NO_CHECKIN (null/未产生打卡事实) 与 ABSENT (status=0 显式缺勤); + * 2. 严格对齐参训学员生命周期签到守卫: + * - 0 待报到: 禁止签到 (必须先 confirmReport) + * - 1 已报到: 允许签到 + * - 2 请假: 允许签到 (Preview Default: PREVIEW_DEFAULT_ALLOWED; PENDING_CUSTOMER_CONFIRMATION) + * - 3 放弃: 禁止签到 + * - 4 已完成: 禁止签到 + * 3. 真实处理后端错误,杜绝假成功。 + */ + +export const ENROLLMENT_STATUS_OPTIONS = [ + { value: 0, label: '待报到' }, + { value: 1, label: '已报到' }, + { value: 2, label: '请假' }, + { value: 3, label: '放弃' }, + { value: 4, label: '已完成' } +]; + +export const ATTENDANCE_STATUS_OPTIONS = [ + { value: 0, label: '缺勤' }, + { value: 1, label: '正常' }, + { value: 2, label: '迟到' }, + { value: 3, label: '早退' }, + { value: 4, label: '请假' } +]; + +/** + * 格式化考勤状态展示标签 (严格区分未签到与缺勤) + * @param {number|null|undefined} status + * @returns {{ text: string, type: string }} + */ +export function formatAttendanceTag(status) { + if (status === null || status === undefined) { + return { text: '未签到', type: 'info' }; + } + switch (Number(status)) { + case 0: + return { text: '缺勤', type: 'danger' }; + case 1: + return { text: '正常', type: 'success' }; + case 2: + return { text: '迟到', type: 'warning' }; + case 3: + return { text: '早退', type: 'warning' }; + case 4: + return { text: '请假', type: 'primary' }; + default: + return { text: '-', type: '' }; + } +} + +/** + * 格式化学员参训状态展示标签 + * @param {number|null|undefined} status + * @returns {{ text: string, type: string }} + */ +export function formatEnrollmentStatusTag(status) { + if (status === null || status === undefined) { + return { text: '-', type: '' }; + } + switch (Number(status)) { + case 0: + return { text: '待报到', type: 'info' }; + case 1: + return { text: '已报到', type: 'success' }; + case 2: + return { text: '请假', type: 'warning' }; + case 3: + return { text: '放弃', type: 'danger' }; + case 4: + return { text: '已完成', type: 'info' }; + default: + return { text: '未知', type: '' }; + } +} + +/** + * 判定参训学员生命周期状态是否允许生成新的签到事实 + * @param {number|null|undefined} status + * @returns {boolean} + */ +export function isEnrollmentEligibleForSignIn(status) { + if (status === null || status === undefined) { + return false; + } + const s = Number(status); + // status 1 (已报到在训) 或 status 2 (请假学员,Preview Default 允许) + return s === 1 || s === 2; +} + +/** + * 计算课次考勤名册单行学员的操作按钮可用性 + * @param {Object} row + * @returns {{ canSignIn: boolean, canSignOut: boolean, canAdjust: boolean }} + */ +export function computeRosterActionEligibility(row) { + if (!row) { + return { canSignIn: false, canSignOut: false, canAdjust: false }; + } + + const hasSignInFact = !!row.signInTime; + const eligibleStatus = isEnrollmentEligibleForSignIn(row.enrollmentStatus); + + // 必须同时满足:无签到事实 + 参训生命周期合法 (非待报到、非已完成、非放弃) + 后端允许标记 + const canSignIn = !hasSignInFact && eligibleStatus && row.canSignIn !== false; + + // 签退:已签到但未签退 + const canSignOut = hasSignInFact && !row.signOutTime && row.canSignOut !== false; + + // 调整:已有打卡记录 + const canAdjust = row.checkinId !== null && row.checkinId !== undefined && row.canAdjust !== false; + + return { canSignIn, canSignOut, canAdjust }; +} + +/** + * 解析签到/打卡操作的后端错误反馈,确保真实回显业务错误,杜绝假成功 + * @param {any} err - 捕获的后端异常对象或字符串 + * @param {string} [defaultName] - 学员名称 + * @returns {{ isSuccess: false, message: string }} + */ +export function resolveSignInFeedback(err, defaultStudentName = '') { + if (err === 'cancel' || err === 'close') { + return { isSuccess: false, message: '操作已取消' }; + } + + let msg = ''; + if (typeof err === 'string') { + msg = err; + } else if (err && typeof err === 'object') { + msg = err.msg || err.message || (err.data && err.data.msg) || '签到操作失败'; + } else { + msg = '签到操作失败'; + } + + // 绝对禁止包含“成功”字样如果其实是错误返回 + return { + isSuccess: false, + message: defaultStudentName ? `【${defaultStudentName}】${msg}` : msg + }; +} diff --git a/build_ui/src/views/train/checkin/index.vue b/build_ui/src/views/train/checkin/index.vue index 624a54f3e..8716b9872 100644 --- a/build_ui/src/views/train/checkin/index.vue +++ b/build_ui/src/views/train/checkin/index.vue @@ -50,7 +50,7 @@ - + + + + @@ -86,7 +89,7 @@