import { get, postJ, putJ } from "@/utils/request"; import { processInstanceCreateAPI, processInstancePage } from "@/api/wt/index.js"; import { withTimeout } from "@/api/hr/attendance"; import Vue from "vue"; // 发起审批的请求超时(ms):避免 utils/request 非 JSON 响应下永远挂起 const REQUEST_TIMEOUT = 12000; // 表单本地 type → 接口 applyType 枚举(HR 侧仅保留以下 5 类) export const ATTENDANCE_APPLY_TYPE_MAP = { leave: "LEAVE", overtime: "OVERTIME", field: "OUTING", // 外勤 trip: "TRIP", check: "CARD_REPAIR", }; // 表单本地 type → 流程业务类型码(与 api/hr/bpm.js 的 BUSINESS_TYPE_LABELS 对齐) export const ATTENDANCE_BUSINESS_TYPE = { leave: "ATTENDANCE_LEAVE", overtime: "ATTENDANCE_OVERTIME", field: "ATTENDANCE_OUTING", trip: "ATTENDANCE_TRIP", check: "ATTENDANCE_CARD_REPAIR", }; /** * 考勤申请审批流程标识 —— 五类申请各用各的,与流程定义 key 一一对应: * 请假 → hr_attendance_leave_apply * 加班 → hr_attendance_overtime_apply * 外勤 → hr_attendance_outing_apply * 出差 → hr_attendance_trip_apply * 补卡 → hr_attendance_card_repair_apply */ export const ATTENDANCE_APPROVAL_BUSINESS_KEYS = { leave: "hr_attendance_leave_apply", overtime: "hr_attendance_overtime_apply", field: "hr_attendance_outing_apply", // 外勤 trip: "hr_attendance_trip_apply", check: "hr_attendance_card_repair_apply", }; export const ATTENDANCE_APPLY_TYPE_KEYS = Object.keys(ATTENDANCE_APPLY_TYPE_MAP); // 表单 leaveType 中文标签 → 接口 leaveType 枚举 export const LEAVE_TYPE_MAP = { 年假: "ANNUAL", 事假: "PERSONAL", 病假: "SICK", 调休假: "COMPENSATORY", 婚假: "MARRIAGE", 产假: "MATERNITY", 陪产假: "PATERNITY", 丧假: "BEREAVEMENT", 工伤假: "WORK_INJURY", 其他: "OTHER", }; // 接口 leaveType 枚举 → 中文标签(详情回填用,与 LEAVE_TYPE_MAP 互为逆映射) export const LEAVE_TYPE_LABEL = Object.keys(LEAVE_TYPE_MAP).reduce((acc, key) => { acc[LEAVE_TYPE_MAP[key]] = key; return acc; }, {}); // 补卡类型枚举(对应后端 repairType 字段),仅 CARD_REPAIR 申请使用 export const REPAIR_TYPE_LABEL = { MISSING_IN: "上班漏打卡", MISSING_OUT: "下班漏打卡", LATE: "迟到补卡", EARLY: "早退补卡", ABSENT: "缺勤补卡", OTHER: "其他补卡", NORMAL: "正常补卡", }; export const REPAIR_TYPE_OPTIONS = Object.keys(REPAIR_TYPE_LABEL); const DEFAULT_START_TIME = "08:30"; const DEFAULT_END_TIME = "17:30"; export function combineDateTime(dateStr, timeStr) { if (!dateStr) return ""; if (!timeStr) return `${dateStr}T00:00:00.000`; return `${dateStr}T${timeStr}:00.000`; } function buildRangeTime(dateStr, timeStr) { if (!dateStr) return ""; return combineDateTime(dateStr, timeStr || "00:00"); } /** 时长下限(分钟):不足 0.5 小时按 0.5 小时计 */ export const MIN_DURATION_MINUTES = 30; /** * 按起止日期时间计算时长(分钟): * - 不足 30 分钟(0.5 小时)按 30 分钟计; * - 起止无效或结束早于开始时返回 0(提交前校验会拦截该情况)。 */ export function computeDurationMinutes(startDate, startTime, endDate, endTime) { if (!startDate || !endDate) return 0; const start = new Date(`${startDate}T${startTime || "00:00"}:00`); const end = new Date(`${endDate}T${endTime || "00:00"}:00`); if (isNaN(start.getTime()) || isNaN(end.getTime())) return 0; if (end < start) return 0; return Math.max(MIN_DURATION_MINUTES, Math.floor((end.getTime() - start.getTime()) / 60000)); } /** 拆分接口时间值为 { date, time }(time 精确到分钟) */ export function splitDateTime(value) { const text = String(value || "").replace("T", " ").trim(); if (!text) return { date: "", time: "" }; const parts = text.split(" "); return { date: parts[0] || "", time: (parts[1] || "").slice(0, 5) }; } /** * 接口数据 → 表单数据(详情/编辑回填) * 入参可以是考勤申请详情 VO,也可以是流程发起时透传的 formVariables(字段同构) */ export function hydrateAttendanceForm(source = {}, type = "leave") { const src = source || {}; const start = splitDateTime(src.startTime); const end = splitDateTime(src.endTime); const attachmentIds = String(src.attachmentFileIds || "") .split(",") .map((item) => item.trim()) .filter(Boolean); const base = { reason: src.reason || "", attachmentIds, }; if (type === "check") { const punch = splitDateTime(src.repairPunchTime || src.startTime); return Object.assign(base, { startDate: punch.date || start.date, repairType: src.repairType || "", repairTime: punch.time || start.time, }); } const form = Object.assign(base, { startDate: start.date, startTime: start.time || DEFAULT_START_TIME, endDate: end.date || start.date, endTime: end.time || DEFAULT_END_TIME, }); if (type === "leave") { const code = String(src.leaveType || "").toUpperCase(); form.leaveType = code ? LEAVE_TYPE_LABEL[code] || "" : ""; } return form; } /** * 把表单数据按 applyType 映射成 /hr/attendance/applications 接口参数。 * @param {object} form 表单数据 * @param {string} type 表单类型(leave / overtime / field / trip / check) * @param {object} userRaw storage 中的 userInfo * @param {string} attachmentFileIds 已上传附件的 id,英文逗号分隔 */ export function buildAttendancePayload(form, type, userRaw, attachmentFileIds) { const applyType = ATTENDANCE_APPLY_TYPE_MAP[type]; if (!applyType) return null; const user = userRaw || {}; const userId = user.userId || user.id || ""; const userName = user.userName || user.name || ""; const base = { userId, userName, applyType, attachmentFileIds: attachmentFileIds || "", }; switch (type) { case "leave": return Object.assign({}, base, { leaveType: LEAVE_TYPE_MAP[form.leaveType] || "OTHER", startTime: buildRangeTime(form.startDate, form.startTime || DEFAULT_START_TIME), endTime: buildRangeTime(form.endDate, form.endTime || DEFAULT_END_TIME), durationMinutes: computeDurationMinutes( form.startDate, form.startTime || DEFAULT_START_TIME, form.endDate, form.endTime || DEFAULT_END_TIME ), reason: form.reason, }); case "overtime": return Object.assign({}, base, { overtimeType: "WORKDAY", startTime: buildRangeTime(form.startDate, form.startTime || "18:00"), endTime: buildRangeTime(form.endDate, form.endTime || "20:00"), durationMinutes: computeDurationMinutes( form.startDate, form.startTime || "18:00", form.endDate, form.endTime || "20:00" ), reason: form.reason, }); case "trip": return Object.assign({}, base, { startTime: buildRangeTime(form.startDate, form.startTime || DEFAULT_START_TIME), endTime: buildRangeTime(form.endDate, form.endTime || DEFAULT_END_TIME), durationMinutes: computeDurationMinutes( form.startDate, form.startTime || DEFAULT_START_TIME, form.endDate, form.endTime || DEFAULT_END_TIME ), needPunch: 0, reason: form.reason, }); case "field": return Object.assign({}, base, { startTime: buildRangeTime(form.startDate, form.startTime || DEFAULT_START_TIME), endTime: buildRangeTime(form.endDate, form.endTime || DEFAULT_END_TIME), durationMinutes: computeDurationMinutes( form.startDate, form.startTime || DEFAULT_START_TIME, form.endDate, form.endTime || DEFAULT_END_TIME ), syncAsPunch: 0, reason: form.reason, }); case "check": return Object.assign({}, base, { startTime: combineDateTime(form.startDate, form.repairTime), repairPunchTime: combineDateTime(form.startDate, form.repairTime), repairType: form.repairType, reason: form.reason, }); default: return null; } } /** * 提取建单接口返回的申请主键(发起审批的 businessId 必须是主键本身)。 * 兼容后端返回:标量("1234567890" / 1234567890)、{ id } / { applicationId } / * { businessId } / { data: { id } } 等结构。 */ export function pickApplicationId(data) { if (data === undefined || data === null || data === "") return ""; if (typeof data === "string" || typeof data === "number") return String(data); const keys = ["id", "applicationId", "businessId", "pkId"]; for (const key of keys) { const value = data[key]; if (value !== undefined && value !== null && value !== "") return String(value); } if (data.data) return pickApplicationId(data.data); return ""; } /** * 提交考勤类申请(请假 / 加班 / 出差 / 外出 / 补卡) * POST /hr/attendance/applications * @returns 接口原始返回(申请主键用 pickApplicationId 提取) */ export async function submitAttendanceApplication(payload, loding = true) { const body = Object.assign({}, payload); if (!body.applyType) body.applyType = "CARD_REPAIR"; const res = await withTimeout( postJ(Vue.prototype.apiUrl + "/hr/attendance/applications", body, loding), REQUEST_TIMEOUT, "提交申请超时,请检查网络后重试(申请可能已保存,请到“我的申请”确认)" ); if (res.code == 0) return res.data || {}; return Promise.reject(new Error(res.message || "申请提交失败")); } /** * 考勤申请详情 * GET /hr/attendance/applications/{id} * 注:后端若未开放该查询,调用方应回退到流程实例的 formVariables(见 loadAttendanceApplicationForEdit) */ export async function getAttendanceApplicationDetail(id, loding = false) { if (id === undefined || id === null || id === "") return null; const res = await get( Vue.prototype.apiUrl + `/hr/attendance/applications/${encodeURIComponent(String(id))}`, {}, loding ); if (res.code == 0) return res.data || null; return Promise.reject(new Error(res.message || "查询考勤申请详情失败")); } /** * 修改被驳回的考勤申请 * PUT /hr/attendance/applications/{id} */ export async function updateAttendanceApplication(id, payload, loding = false) { if (id === undefined || id === null || id === "") throw new Error("缺少申请主键"); const body = Object.assign({}, payload); if (!body.applyType) body.applyType = "CARD_REPAIR"; const res = await putJ( Vue.prototype.apiUrl + `/hr/attendance/applications/${encodeURIComponent(String(id))}`, body, loding ); if (res.code == 0) return res.data; return Promise.reject(new Error(res.message || "考勤申请修改失败")); } /** * 编辑态取数:优先用考勤申请详情接口,失败时回退到流程实例的 formVariables * (发起审批时已把整份载荷作为 variables 透传,字段与详情 VO 同构) */ export async function loadAttendanceApplicationForEdit(businessId, processInstanceId = "") { if (businessId !== undefined && businessId !== null && businessId !== "") { try { const detail = await getAttendanceApplicationDetail(businessId, false); if (detail && (detail.startTime || detail.reason || detail.applyType)) return detail; } catch (error) { // 详情接口不可用时走流程变量兜底 } } if (!processInstanceId) return null; const res = await get( Vue.prototype.apiUrl + `/bpm/process-instance/get?id=${encodeURIComponent(String(processInstanceId))}`, {}, false ); if (res.code == 0) return (res.data && res.data.formVariables) || null; return null; } /** * 发起考勤申请审批流程。 * * 五类申请各用各的流程标识(ATTENDANCE_APPROVAL_BUSINESS_KEYS[type]), * 与其它业务流程一致走两段式: * 1. GET 流程定义列表(按 key 匹配)取 processDefinition.id; * 2. POST /bpm/process-instance/create,带 processDefinitionId。 * businessId 为考勤申请主键;variables 同时作为审批详情页的表单数据来源。 * * @param {string} businessId 考勤申请主键 * @param {object} variables 申请载荷(含 businessType / businessName / deptName 等) * @param {boolean} loding 历史参数(loading 由调用方控制),保留以兼容既有调用方式 * @param {string} type 表单类型(leave / overtime / field / trip / check),决定用哪个流程标识 */ export async function startAttendanceApproval(businessId, variables = {}, loding = false, type = "") { if (!businessId) throw new Error("缺少申请主键,无法发起审批"); const businessKey = ATTENDANCE_APPROVAL_BUSINESS_KEYS[type]; if (!businessKey) throw new Error(`未知的考勤申请类型:${type || "(空)"},无法发起审批`); // 两段式第 1 步:按 key 查流程定义,取最新一条的 processDefinition.id const page = await withTimeout( processInstancePage( { pageNo: 1, pageSize: 1, reset: true, key: businessKey }, false ), REQUEST_TIMEOUT, "查询审批流程定义超时,请检查网络后重试" ); const first = page && page.list && page.list[0]; const processDefinitionId = first && first.processDefinition ? first.processDefinition.id : ""; if (!processDefinitionId) { throw new Error(`未匹配到审批流程(${businessKey}),请联系管理员配置流程定义`); } // 两段式第 2 步:发起流程实例 const params = { businessId: String(businessId), businessKey, formCreateUserId: variables.userId || "", processDefinitionId, variables, }; console.log( "[考勤审批] POST /bpm/process-instance/create:", type || variables.businessType || "-", JSON.stringify(params) ); return withTimeout( processInstanceCreateAPI(params, false), REQUEST_TIMEOUT, `发起审批超时(${businessKey}),请到“我的申请”确认是否已提交` ); }