| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- 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}),请到“我的申请”确认是否已提交`
- );
- }
|