|
|
@@ -21,147 +21,318 @@ function dateOnly(value) {
|
|
|
return String(value).slice(0, 10);
|
|
|
}
|
|
|
|
|
|
-/** 录用审批状态码 → 中文 */
|
|
|
+function toNumber(value) {
|
|
|
+ if (value === null || value === undefined || value === "") return undefined;
|
|
|
+ if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
|
+ const matched = String(value).replace(/,/g, "").match(/(\d+(?:\.\d+)?)/);
|
|
|
+ return matched ? Number(matched[1]) : undefined;
|
|
|
+}
|
|
|
+
|
|
|
+function toProbationMonths(value) {
|
|
|
+ if (value === null || value === undefined || value === "") return undefined;
|
|
|
+ if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
|
+ const matched = String(value).match(/(\d+)/);
|
|
|
+ return matched ? Number(matched[1]) : undefined;
|
|
|
+}
|
|
|
+
|
|
|
+/** 大整数主键保持字符串,避免雪花 ID 精度丢失 */
|
|
|
+function keepBigIntId(value) {
|
|
|
+ if (value === undefined || value === null || value === "") return undefined;
|
|
|
+ const text = String(value).trim();
|
|
|
+ if (!/^-?\d+$/.test(text)) return value;
|
|
|
+ if (!Number.isSafeInteger(Number(text))) return text;
|
|
|
+ return Number(text);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 录用审批状态(文档:PENDING / APPROVED / REJECTED)
|
|
|
+ * 未发起流程、无状态码时前端按草稿展示
|
|
|
+ */
|
|
|
export const HIRING_STATUS_LABELS = {
|
|
|
DRAFT: "草稿",
|
|
|
- PENDING_MY_APPROVAL: "待我审批",
|
|
|
- APPROVING: "审批中",
|
|
|
+ PENDING: "审批中",
|
|
|
APPROVED: "已通过",
|
|
|
REJECTED: "已拒绝",
|
|
|
};
|
|
|
|
|
|
+export const HIRING_STATUS_OPTIONS = Object.entries(HIRING_STATUS_LABELS).map(
|
|
|
+ ([value, label]) => ({ value, label }),
|
|
|
+);
|
|
|
+
|
|
|
/** 已通过:可发起 Offer */
|
|
|
-export const HIRING_APPROVED_STATUS = new Set(["APPROVED", "已通过"]);
|
|
|
+export const HIRING_APPROVED_STATUS = new Set(["APPROVED"]);
|
|
|
+
|
|
|
+/** BPM 流程标识,需与流程定义 code 一致;提交审核弹框按此拉取流程分类/发起流程 */
|
|
|
+export const HIRING_APPROVAL_BUSINESS_KEY = "hr_recruit_hiring_approval";
|
|
|
|
|
|
export function normalizeHiringStatus(raw = {}) {
|
|
|
- const code = String(
|
|
|
- raw.approvalStatus || raw.status || raw.statusCode || "",
|
|
|
- ).toUpperCase();
|
|
|
- const labelMap = {
|
|
|
- DRAFT: "DRAFT",
|
|
|
- 草稿: "DRAFT",
|
|
|
- PENDING_MY_APPROVAL: "PENDING_MY_APPROVAL",
|
|
|
- WAITING: "PENDING_MY_APPROVAL",
|
|
|
- 待我审批: "PENDING_MY_APPROVAL",
|
|
|
- APPROVING: "APPROVING",
|
|
|
- PENDING_APPROVAL: "APPROVING",
|
|
|
- 审批中: "APPROVING",
|
|
|
- APPROVED: "APPROVED",
|
|
|
- PASSED: "APPROVED",
|
|
|
- 已通过: "APPROVED",
|
|
|
- REJECTED: "REJECTED",
|
|
|
- 已拒绝: "REJECTED",
|
|
|
- 已驳回: "REJECTED",
|
|
|
- };
|
|
|
- return labelMap[code] || labelMap[raw.approvalStatus] || code || "DRAFT";
|
|
|
+ const code = String(raw.approvalStatus || raw.status || "").trim().toUpperCase();
|
|
|
+ if (code === "APPROVED" || code === "PASSED") return "APPROVED";
|
|
|
+ if (code === "REJECTED" || code === "已拒绝" || code === "已驳回") return "REJECTED";
|
|
|
+ if (
|
|
|
+ code === "PENDING" ||
|
|
|
+ code === "APPROVING" ||
|
|
|
+ code === "PENDING_APPROVAL" ||
|
|
|
+ code === "WAITING"
|
|
|
+ ) {
|
|
|
+ return "PENDING";
|
|
|
+ }
|
|
|
+ if (raw.processInstanceId) return "PENDING";
|
|
|
+ if (raw.id) return "DRAFT";
|
|
|
+ return "DRAFT";
|
|
|
}
|
|
|
|
|
|
-export function adaptHiringApproval(raw = {}) {
|
|
|
+/** 适配「录用审批视图」+ 前端展示扩展字段 */
|
|
|
+export function adaptHiringApproval(raw = {}, extras = {}) {
|
|
|
const statusCode = normalizeHiringStatus(raw);
|
|
|
const hireDate =
|
|
|
- dateOnly(raw.plannedHireDate || raw.hireDate || raw.onboardingDate) || "";
|
|
|
- const experienceRaw = raw.experience ?? raw.workYears ?? "";
|
|
|
+ dateOnly(raw.expectedEntryDate || extras.expectedEntryDate || raw.hireDate) ||
|
|
|
+ "";
|
|
|
+ const departmentId =
|
|
|
+ extras.departmentId ||
|
|
|
+ raw.hiringDeptId ||
|
|
|
+ raw.departmentId ||
|
|
|
+ raw.deptId;
|
|
|
+ const departmentName =
|
|
|
+ extras.departmentName ||
|
|
|
+ raw.hiringDeptName ||
|
|
|
+ raw.departmentName ||
|
|
|
+ raw.deptName ||
|
|
|
+ "";
|
|
|
+ const phone =
|
|
|
+ extras.phone ||
|
|
|
+ extras.mobile ||
|
|
|
+ raw.contactMobile ||
|
|
|
+ raw.phone ||
|
|
|
+ raw.mobile ||
|
|
|
+ "";
|
|
|
return {
|
|
|
...raw,
|
|
|
id: raw.id,
|
|
|
- approvalNo: raw.approvalNo || (raw.id ? `HA${raw.id}` : ""),
|
|
|
- candidateId: raw.candidateId,
|
|
|
- candidateName: raw.candidateName || raw.name || "",
|
|
|
- gender: raw.gender || raw.title || "",
|
|
|
- education: raw.education || "",
|
|
|
- phone: raw.phone || raw.mobile || raw.candidatePhone || "",
|
|
|
- experience: experienceRaw,
|
|
|
- positionId: raw.positionId,
|
|
|
- positionName: raw.positionName || raw.appliedPosition || "",
|
|
|
- departmentId: raw.departmentId || raw.recruitmentDeptId || raw.deptId,
|
|
|
- departmentName:
|
|
|
- raw.departmentName ||
|
|
|
- raw.recruitmentDeptName ||
|
|
|
- raw.deptName ||
|
|
|
- raw.department ||
|
|
|
- "",
|
|
|
- proposedSalary: raw.proposedSalary || raw.salaryAdvice || raw.salary || "",
|
|
|
- probation: raw.probation || raw.probationPeriod || "",
|
|
|
- salaryStructure: raw.salaryStructure || "",
|
|
|
- staffingType:
|
|
|
+ approvalNo:
|
|
|
+ raw.approvalNo ||
|
|
|
+ raw.documentCode ||
|
|
|
+ (raw.id != null ? `HA${raw.id}` : ""),
|
|
|
+ documentCode: raw.documentCode || "",
|
|
|
+ applicationId: raw.applicationId ?? extras.applicationId,
|
|
|
+ candidateId:
|
|
|
+ raw.candidateId ??
|
|
|
+ extras.candidateId ??
|
|
|
+ raw.userId ??
|
|
|
+ extras.userId,
|
|
|
+ userId: raw.userId ?? extras.userId ?? raw.candidateId,
|
|
|
+ candidateName:
|
|
|
+ extras.candidateName || raw.candidateName || raw.name || "",
|
|
|
+ positionId: raw.positionId ?? extras.positionId,
|
|
|
+ positionName: raw.positionName || extras.positionName || "",
|
|
|
+ hiringDeptId: raw.hiringDeptId ?? departmentId,
|
|
|
+ hiringDeptName: raw.hiringDeptName || departmentName,
|
|
|
+ departmentId,
|
|
|
+ departmentName,
|
|
|
+ gender: extras.gender || raw.gender || "",
|
|
|
+ education: extras.education ?? raw.education ?? "",
|
|
|
+ phone,
|
|
|
+ mobile: phone,
|
|
|
+ contactMobile: raw.contactMobile || phone,
|
|
|
+ proposedSalary: raw.proposedSalary ?? extras.proposedSalary,
|
|
|
+ workYears:
|
|
|
+ extras.workYears ??
|
|
|
+ raw.workYears ??
|
|
|
+ extras.experienceYears ??
|
|
|
+ raw.experienceYears,
|
|
|
+ employmentType:
|
|
|
+ extras.employmentType ||
|
|
|
+ raw.employmentType ||
|
|
|
raw.staffingType ||
|
|
|
- raw.positionNature ||
|
|
|
- raw.positionType ||
|
|
|
- raw.编制类型 ||
|
|
|
"",
|
|
|
+ staffingType: extras.employmentType || raw.employmentType || raw.staffingType || "",
|
|
|
+ probationPeriod: raw.probationPeriod ?? extras.probationPeriod,
|
|
|
+ probation:
|
|
|
+ (raw.probationPeriod ?? extras.probationPeriod) != null &&
|
|
|
+ (raw.probationPeriod ?? extras.probationPeriod) !== ""
|
|
|
+ ? `${raw.probationPeriod ?? extras.probationPeriod}个月`
|
|
|
+ : extras.probation || raw.probation || "",
|
|
|
hireDate,
|
|
|
- currentNode: raw.currentNode || raw.nodeName || raw.approvalNode || "-",
|
|
|
+ expectedEntryDate: hireDate,
|
|
|
+ workLocation: extras.workLocation || raw.workLocation || "",
|
|
|
+ specialTerms: extras.specialTerms || raw.specialTerms || raw.remark || "",
|
|
|
+ remark: extras.specialTerms || raw.specialTerms || raw.remark || "",
|
|
|
+ specialApprovalReason: raw.specialApprovalReason || "",
|
|
|
+ processInstanceId: raw.processInstanceId || "",
|
|
|
+ currentNode: extras.currentNode || raw.currentNode || raw.nodeName || "-",
|
|
|
+ resumeAttachmentFileIds:
|
|
|
+ raw.resumeAttachmentFileIds || extras.resumeAttachmentFileIds || "",
|
|
|
statusCode,
|
|
|
status: HIRING_STATUS_LABELS[statusCode] || raw.approvalStatus || "-",
|
|
|
- remark: raw.remark || raw.approvalComment || "",
|
|
|
- attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
|
|
|
- applicationId: raw.applicationId,
|
|
|
offerId: raw.offerId,
|
|
|
- offerSent: Boolean(raw.offerId || raw.offerSent),
|
|
|
+ offerSent: Boolean(raw.offerId || extras.offerSent),
|
|
|
+ createTime: raw.createTime,
|
|
|
+ createUserId: raw.createUserId,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+function toEducationValue(value) {
|
|
|
+ if (value === null || value === undefined || value === "") return undefined;
|
|
|
+ const text = String(value).trim();
|
|
|
+ return text || undefined;
|
|
|
+}
|
|
|
+
|
|
|
+export function parseResumeAttachmentFileIds(value) {
|
|
|
+ if (Array.isArray(value)) {
|
|
|
+ return value
|
|
|
+ .map((item) => String(item ?? "").trim())
|
|
|
+ .filter(Boolean);
|
|
|
+ }
|
|
|
+ if (value == null || value === "") return [];
|
|
|
+ return String(value)
|
|
|
+ .split(",")
|
|
|
+ .map((item) => item.trim())
|
|
|
+ .filter(Boolean);
|
|
|
+}
|
|
|
+
|
|
|
+function joinResumeFileIds(form = {}) {
|
|
|
+ const files = Array.isArray(form.attachmentFiles)
|
|
|
+ ? form.attachmentFiles
|
|
|
+ : Array.isArray(form.attachments)
|
|
|
+ ? form.attachments
|
|
|
+ : null;
|
|
|
+ if (files) {
|
|
|
+ const ids = files
|
|
|
+ .map((item) => keepBigIntId(item.id || item.fileId))
|
|
|
+ .filter((id) => id != null && id !== "");
|
|
|
+ return ids.length ? ids.join(",") : undefined;
|
|
|
+ }
|
|
|
+ if (form.resumeAttachmentFileIds) {
|
|
|
+ return (
|
|
|
+ parseResumeAttachmentFileIds(form.resumeAttachmentFileIds).join(",") ||
|
|
|
+ undefined
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 保存草稿请求体 → 录用审批新增参数
|
|
|
+ * 必填:expectedEntryDate、proposedSalary
|
|
|
+ * 线下(无 applicationId):name、positionId 必填
|
|
|
+ */
|
|
|
export function serializeHiringApproval(form = {}) {
|
|
|
- const experienceYears =
|
|
|
- form.experience === null ||
|
|
|
- form.experience === undefined ||
|
|
|
- form.experience === ""
|
|
|
- ? undefined
|
|
|
- : Number(form.experience);
|
|
|
- const experience =
|
|
|
- experienceYears == null || Number.isNaN(experienceYears)
|
|
|
- ? undefined
|
|
|
- : `${experienceYears}年`;
|
|
|
- return compact({
|
|
|
- id: form.id || undefined,
|
|
|
- candidateId: form.candidateId || undefined,
|
|
|
- candidateName: form.candidateName || form.name,
|
|
|
+ const applicationId = keepBigIntId(form.applicationId);
|
|
|
+ const name = String(form.name || form.candidateName || "").trim();
|
|
|
+ const employmentType = keepBigIntId(form.employmentType ?? form.staffingType);
|
|
|
+ const candidateId = keepBigIntId(
|
|
|
+ form.candidateId ?? form.userId ?? form.personId,
|
|
|
+ );
|
|
|
+ const payload = compact({
|
|
|
+ id: keepBigIntId(form.id),
|
|
|
+ applicationId,
|
|
|
+ candidateId,
|
|
|
+ contactMobile: String(
|
|
|
+ form.contactMobile || form.phone || form.mobile || "",
|
|
|
+ ).trim() || undefined,
|
|
|
+ demandId: keepBigIntId(form.demandId),
|
|
|
+ education: toEducationValue(form.education),
|
|
|
+ employmentType,
|
|
|
+ expectedEntryDate: dateOnly(form.expectedEntryDate || form.hireDate),
|
|
|
gender: form.gender || undefined,
|
|
|
- education: form.education || undefined,
|
|
|
- phone: form.phone || undefined,
|
|
|
- experience,
|
|
|
- workYears: experienceYears,
|
|
|
- positionId: form.positionId || undefined,
|
|
|
- positionName: form.positionName || undefined,
|
|
|
- departmentId: form.departmentId || undefined,
|
|
|
- departmentName: form.departmentName || undefined,
|
|
|
- proposedSalary: form.proposedSalary || undefined,
|
|
|
- probation: form.probation || undefined,
|
|
|
- plannedHireDate: dateOnly(form.hireDate),
|
|
|
- salaryStructure: form.salaryStructure || undefined,
|
|
|
- staffingType: form.staffingType || undefined,
|
|
|
- remark: form.remark || undefined,
|
|
|
- applicationId: form.applicationId || undefined,
|
|
|
- attachmentFileIds: Array.isArray(form.attachmentFileIds)
|
|
|
- ? form.attachmentFileIds
|
|
|
- : undefined,
|
|
|
+ name: name || undefined,
|
|
|
+ positionId: keepBigIntId(form.positionId),
|
|
|
+ probationPeriod: toProbationMonths(form.probationPeriod ?? form.probation),
|
|
|
+ proposedLevel: form.proposedLevel || undefined,
|
|
|
+ proposedSalary: (() => {
|
|
|
+ const amount = toNumber(form.proposedSalary);
|
|
|
+ return amount == null ? undefined : Math.round(amount);
|
|
|
+ })(),
|
|
|
+ resumeAttachmentFileIds: joinResumeFileIds(form),
|
|
|
+ workYears: (() => {
|
|
|
+ const years = toNumber(form.workYears);
|
|
|
+ return years == null ? undefined : Math.round(years);
|
|
|
+ })(),
|
|
|
+ });
|
|
|
+ if (payload.applicationId == null || payload.applicationId === "") {
|
|
|
+ delete payload.applicationId;
|
|
|
+ }
|
|
|
+ payload.workLocation = String(form.workLocation || "").trim();
|
|
|
+ payload.specialTerms = String(form.specialTerms || form.remark || "").trim();
|
|
|
+ return payload;
|
|
|
+}
|
|
|
+
|
|
|
+/** 校验保存草稿必填(文档:expectedEntryDate、proposedSalary;线下须 name、positionId) */
|
|
|
+export function validateOfflineHiringDraft(form = {}) {
|
|
|
+ const errors = [];
|
|
|
+ if (!dateOnly(form.expectedEntryDate || form.hireDate)) {
|
|
|
+ errors.push("请选择预计入职日期");
|
|
|
+ }
|
|
|
+ if (toNumber(form.proposedSalary) == null) {
|
|
|
+ errors.push("请输入拟定薪资");
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ !keepBigIntId(form.applicationId) &&
|
|
|
+ !String(form.name || form.candidateName || "").trim()
|
|
|
+ ) {
|
|
|
+ errors.push("请填写姓名");
|
|
|
+ }
|
|
|
+ if (!keepBigIntId(form.applicationId) && !keepBigIntId(form.positionId)) {
|
|
|
+ errors.push("请选择应聘岗位");
|
|
|
+ }
|
|
|
+ return errors;
|
|
|
+}
|
|
|
+
|
|
|
+function toQueryInt(value) {
|
|
|
+ if (value === null || value === undefined || value === "") return undefined;
|
|
|
+ if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
|
+ const text = String(value).trim();
|
|
|
+ if (!/^-?\d+$/.test(text)) return undefined;
|
|
|
+ return keepBigIntId(text);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 分页查询参数(GET /recruit/hiring-approvals)
|
|
|
+ * approvalStatus / deptId / keyword / offset / orderBy / pageNum / size / sortName
|
|
|
+ */
|
|
|
+export function serializeHiringApprovalQuery(params = {}) {
|
|
|
+ return compact({
|
|
|
+ approvalStatus: params.approvalStatus || undefined,
|
|
|
+ deptId: toQueryInt(params.deptId),
|
|
|
+ keyword: params.keyword || undefined,
|
|
|
+ offset: toQueryInt(params.offset),
|
|
|
+ orderBy: params.orderBy || undefined,
|
|
|
+ pageNum: toQueryInt(params.pageNum),
|
|
|
+ size: toQueryInt(params.size),
|
|
|
+ sortName: params.sortName || undefined,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+// —— 人力资源-录用审批管理 ——
|
|
|
+
|
|
|
+/** 录用审批分页查询 GET /recruit/hiring-approvals */
|
|
|
export const getHiringApprovalPage = async (params = {}) =>
|
|
|
unwrap(
|
|
|
- await request.get("/recruit/hiring-approvals", { params: compact(params) }),
|
|
|
+ await request.get("/recruit/hiring-approvals", {
|
|
|
+ params: serializeHiringApprovalQuery(params),
|
|
|
+ }),
|
|
|
);
|
|
|
|
|
|
-export const getHiringApprovalById = async (id) =>
|
|
|
- adaptHiringApproval(
|
|
|
- await unwrap(await request.get(`/recruit/hiring-approvals/${id}`)),
|
|
|
+/** 录用审批详情查询 GET /recruit/hiring-approvals/{id},id 为列表主键 */
|
|
|
+export const getHiringApprovalById = async (id) => {
|
|
|
+ const hiringId = keepBigIntId(id);
|
|
|
+ if (hiringId == null || hiringId === "") {
|
|
|
+ return Promise.reject(new Error("缺少录用审批主键"));
|
|
|
+ }
|
|
|
+ return adaptHiringApproval(
|
|
|
+ await unwrap(await request.get(`/recruit/hiring-approvals/${hiringId}`)),
|
|
|
);
|
|
|
+};
|
|
|
|
|
|
-export const createHiringApproval = async (data) =>
|
|
|
- unwrap(await request.post("/recruit/hiring-approvals", data));
|
|
|
+/**
|
|
|
+ * 保存录用审批草稿 POST /recruit/hiring-approvals
|
|
|
+ * 成功返回主键 id;前端发起时再调 BPM create
|
|
|
+ */
|
|
|
+export const createHiringApproval = async (data) => {
|
|
|
+ const payload = serializeHiringApproval(data || {});
|
|
|
+ return unwrap(await request.post("/recruit/hiring-approvals", payload));
|
|
|
+};
|
|
|
|
|
|
+/** 与 create 同一接口(文档无独立修改接口) */
|
|
|
export const updateHiringApproval = async (id, data) =>
|
|
|
- unwrap(await request.put(`/recruit/hiring-approvals/${id}`, data));
|
|
|
-
|
|
|
-export const submitHiringApproval = async (id, data = {}) =>
|
|
|
- unwrap(await request.post(`/recruit/hiring-approvals/${id}/submit`, data));
|
|
|
-
|
|
|
-export const approveHiringApproval = async (id, data = {}) =>
|
|
|
- unwrap(await request.post(`/recruit/hiring-approvals/${id}/approve`, data));
|
|
|
-
|
|
|
-export const rejectHiringApproval = async (id, data = {}) =>
|
|
|
- unwrap(await request.post(`/recruit/hiring-approvals/${id}/reject`, data));
|
|
|
-
|
|
|
-export const deleteHiringApproval = async (id) =>
|
|
|
- unwrap(await request.delete(`/recruit/hiring-approvals/${id}`));
|
|
|
+ createHiringApproval({ ...(data || {}), id: id ?? data?.id });
|