Kaynağa Gözat

feat(劳动合同审批): 三类合同审批详情统一改走 approval-detail 接口

签订续签/变更/解除终止分别传各自 businessKey,由服务端按 businessId + businessKey 解析原合同;解除终止按解除单主键精确匹配台账,未命中时明确报错;变更页拆分变更前后附件并补状态中文兜底
lihaiting 5 gün önce
ebeveyn
işleme
ad5d5bb066

+ 15 - 0
src/api/hr/index.js

@@ -964,6 +964,21 @@ export const renewContract = async (id, data) =>
   unwrap(await request.post(`/hr/contracts/${id}/renew`, data));
 export const terminateContract = async (id, data) =>
   unwrap(await request.post(`/hr/contracts/${id}/termination`, data));
+/**
+ * 查询合同审批完整详情 GET /hr/contracts/approval-detail
+ * 审批中心(本系统)查看合同详情专用:服务端按 businessId + businessKey 解析原合同主键,
+ * 前端不需要知道合同 id,也不需要从流程变量里猜。
+ *  - businessKey = hr_contract_approval           businessId = 合同主键(签订 / 续签)
+ *  - businessKey = hr_contract_change_approval     businessId = 变更单主键
+ *  - businessKey = hr_contract_termination_approval businessId = 解除终止单主键
+ * 响应 = 劳动合同完整档案(与 GET /hr/contracts/{id} 同模型,含 changes / terminations 台账)。
+ */
+export const getContractApprovalDetail = async (businessId, businessKey) =>
+  unwrap(
+    await request.get("/hr/contracts/approval-detail", {
+      params: compact({ businessId, businessKey }),
+    })
+  );
 export const archiveContractOffline = async (id, data) =>
   unwrap(await request.post(`/hr/contracts/${id}/offline/archive`, data));
 export const voidContract = async (id, data) =>

+ 76 - 76
src/views/bpm/handleTask/components/hrChangeContractManagement/hrChangeContractDetail.vue

@@ -20,8 +20,8 @@
             </div>
             <div class="cm-compare-item is-span-2">
               <span class="cm-compare-label">合同附件:</span><span class="cm-compare-value">
-                <fileMain v-if="detailRow.attachmentFileIds.length" :value="detailRow.attachmentFileIds" type="view"
-                  size="small" />
+                <fileMain v-if="detailRow.beforeAttachmentFileIds.length" :value="detailRow.beforeAttachmentFileIds"
+                  type="view" size="small" />
                 <span v-else class="cm-detail-muted">暂无附件</span>
               </span>
             </div>
@@ -37,8 +37,8 @@
             </div>
             <div class="cm-compare-item is-span-2">
               <span class="cm-compare-label">合同附件:</span><span class="cm-compare-value">
-                <fileMain v-if="detailRow.attachmentFileIds.length" :value="detailRow.attachmentFileIds" type="view"
-                  size="small" />
+                <fileMain v-if="detailRow.afterAttachmentFileIds.length" :value="detailRow.afterAttachmentFileIds"
+                  type="view" size="small" />
                 <span v-else class="cm-detail-muted">暂无附件</span>
               </span>
             </div>
@@ -80,16 +80,29 @@
 </template>
 
 <script>
-import { getContractById } from '@/api/hr';
-import { getProcessInstance } from '@/api/bpm/processInstance';
+import { getContractApprovalDetail } from '@/api/hr';
 import { parseAttachmentIds } from '@/utils/attachment';
 
+/** 本组件只服务「劳动合同变更审批」,businessKey 固定 */
+const CHANGE_BUSINESS_KEY = 'hr_contract_change_approval';
+
 /** 签署方式:ONLINE-审批待办附件盖章签字;OFFLINE-线下签署 */
 const SIGNING_METHOD_LABELS = {
   ONLINE: '线上',
   OFFLINE: '线下'
 };
 
+/** 变更单业务状态(DRAFT/PENDING_APPROVAL/SIGNING/EFFECTIVE/REJECTED/CANCELLED):
+    后端 changeStatusName 缺失时按已知编码翻中文,未知编码原样回显 */
+const CHANGE_STATUS_LABELS = {
+  DRAFT: '草稿',
+  PENDING_APPROVAL: '审批中',
+  SIGNING: '签署中',
+  EFFECTIVE: '已生效',
+  REJECTED: '已驳回',
+  CANCELLED: '已取消'
+};
+
 /** 对比字段:快照字段名与合同顶层字段同名,缺失时逐层回退 */
 const FIELD_DEFS = [
   { key: 'employeeName', label: '签订员工' },
@@ -118,9 +131,19 @@ function parseSnapshot(raw) {
   }
 }
 
+/** 快照日期可能是秒(10 位)/毫秒(13 位)时间戳或日期串 → 统一转本地时区 YYYY-MM-DD */
 function formatDate(value) {
   if (!value) return '';
-  return String(value).slice(0, 10);
+  const str = String(value).trim();
+  if (/^\d{10}$/.test(str)) return tsToYMD(Number(str) * 1000);
+  if (/^\d{13}$/.test(str)) return tsToYMD(Number(str));
+  return str.slice(0, 10);
+}
+
+function tsToYMD(ms) {
+  const d = new Date(ms);
+  const pad = (n) => String(n).padStart(2, '0');
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
 }
 
 function formatSalary(value) {
@@ -140,12 +163,13 @@ function formatProbation(value) {
 export default {
   name: 'HrChangeContractDetail',
   props: {
+    /** 流程业务主键:变更审批即变更单主键(不是合同主键) */
     businessId: {
       default: ''
     },
-    /** 流程实例 id:用于从流程变量里补取合同 id */
-    id: {
-      default: ''
+    /** 业务标识,默认取本组件对应的流程 */
+    businessKey: {
+      default: CHANGE_BUSINESS_KEY
     }
   },
   data() {
@@ -170,17 +194,22 @@ export default {
     }
   },
   methods: {
+    /**
+     * 审批详情统一走 GET /hr/contracts/approval-detail:
+     * 服务端按 businessKey + businessId(变更单主键)解析出原合同,返回合同完整档案(含 changes 台账)。
+     * 前端不再用变更单 id 去猜合同 id,也不再从流程变量里补取。
+     */
     async getInfo(businessId) {
       if (!businessId) return;
       this.loading = true;
       try {
-        let contract = await this.fetchContract(businessId);
-        // businessId 可能是变更单 id(不是合同 id):拿到的不对时从流程变量补取合同 id
-        if (!contract || String(contract.id) !== String(businessId)) {
-          const contractId = await this.resolveContractIdFromProcess();
-          if (contractId) contract = await this.fetchContract(contractId);
+        const contract = await getContractApprovalDetail(
+          businessId,
+          this.businessKey
+        );
+        if (!contract) {
+          throw new Error('未获取到合同信息,请后端确认审批详情的返回');
         }
-        if (!contract) throw new Error('未获取到合同信息');
         this.detailRow = this.adaptChange(contract, businessId);
       } catch (e) {
         this.detailRow = null;
@@ -189,63 +218,19 @@ export default {
         this.loading = false;
       }
     },
-    async fetchContract(id) {
-      try {
-        return (await getContractById(id)) || null;
-      } catch (e) {
-        return null;
-      }
-    },
-    /**
-     * 从流程变量补取合同 id(项目内既有做法,见 api/hr/attendanceApply.js)。
-     * 兼容常见变量命名,并打印变量内容便于确认后端实际下发的字段名。
-     */
-    async resolveContractIdFromProcess() {
-      if (!this.id) return '';
-      try {
-        const instance = await getProcessInstance(this.id);
-        const raw = instance && instance.formVariables;
-        const vars = typeof raw === 'string' ? parseSnapshot(raw) : raw || {};
-        // eslint-disable-next-line no-console
-        console.log('[HrChangeContractDetail] 流程变量 formVariables:', vars);
-        const candidates = [
-          vars.contractId,
-          vars.contract_id,
-          vars.contractPrimaryKey,
-          vars.businessContractId,
-          vars.contract && vars.contract.id
-        ];
-        const hit = candidates.find((item) => item != null && item !== '');
-        return hit != null ? String(hit) : '';
-      } catch (e) {
-        return '';
-      }
-    },
-    /**
-     * 合同详情接口返回该合同全部变更记录(changes),定位本条审批对应的变更:
-     * ① 传入的 businessId(后端流程 businessKey 实际存的是变更单 id)
-     * ② contract.changeId
-     * ③ 审批中/签署中的那条
-     * ④ 最新一条
-     */
-    adaptChange(contract = {}, changeIdHint = '') {
+    /** 在合同变更台账里按 businessId(=变更单主键)精确取本条变更,取不到直接报错,不猜 */
+    adaptChange(contract = {}, changeId = '') {
       const list = Array.isArray(contract.changes) ? contract.changes : [];
-      const pendingStatuses = ['PENDING_APPROVAL', 'SIGNING'];
-      const record =
-        list.find((item) => String(item.id) === String(changeIdHint)) ||
-        list.find((item) => String(item.id) === String(contract.changeId)) ||
-        list.find((item) =>
-          pendingStatuses.includes(
-            String(item.changeStatus || '').toUpperCase()
-          )
-        ) ||
-        list[0] ||
-        {};
-      const before = {
-        ...this.contractSnapshot(contract),
-        ...parseSnapshot(record.beforeSnapshot)
-      };
-      const after = { ...before, ...parseSnapshot(record.afterSnapshot) };
+      const record = list.find((item) => String(item.id) === String(changeId));
+      if (!record) {
+        throw new Error(
+          `合同变更台账中未找到变更单(id: ${changeId}),请后端确认 approval-detail 是否返回该变更单`
+        );
+      }
+      const beforeSnapshot = parseSnapshot(record.beforeSnapshot);
+      const afterSnapshot = parseSnapshot(record.afterSnapshot);
+      const before = { ...this.contractSnapshot(contract), ...beforeSnapshot };
+      const after = { ...before, ...afterSnapshot };
       const normalize = (snap) => ({
         employeeName: snap.employeeName || '',
         contractNo: snap.contractNo || '',
@@ -262,7 +247,7 @@ export default {
         endDate: formatDate(snap.endDate),
         signingMethodText:
           SIGNING_METHOD_LABELS[
-          String(snap.signingMethod || '').toUpperCase()
+            String(snap.signingMethod || '').toUpperCase()
           ] ||
           snap.signingMethodName ||
           snap.signingMethod ||
@@ -282,15 +267,30 @@ export default {
         contractNo: contract.contractNo || beforeView.contractNo,
         beforeFields: buildFields(beforeView, null),
         afterFields: buildFields(afterView, beforeView),
-        attachmentFileIds: parseAttachmentIds(contract),
+        /** 原合同附件:合同档案 + 变更前快照 */
+        beforeAttachmentFileIds: parseAttachmentIds({
+          ...contract,
+          ...beforeSnapshot
+        }),
+        /** 变更后附件:变更单顶层 attachmentIds 与变更后快照合并,快照优先 */
+        afterAttachmentFileIds: parseAttachmentIds({
+          ...record,
+          ...afterSnapshot
+        }),
         changeNo: record.changeNo || '',
         changeTypeText: record.changeTypeName || record.changeType || '-',
-        changeStatusText: record.changeStatusName || record.changeStatus || '-',
+        changeStatusText:
+          record.changeStatusName ||
+          CHANGE_STATUS_LABELS[
+            String(record.changeStatus || '').toUpperCase()
+          ] ||
+          record.changeStatus ||
+          '-',
         effectiveDate: formatDate(record.effectiveDate),
         changeReason: record.changeReason || ''
       };
     },
-    /** 合同顶层快照字段:作为变更前快照的兜底来源 */
+    /** 合同顶层快照字段:变更前快照里没有的字段,用合同当前值补齐对比基准 */
     contractSnapshot(contract) {
       return {
         employeeName: contract.employeeName,

+ 12 - 3
src/views/bpm/handleTask/components/hrContractManagement/hrContractDetail.vue

@@ -171,7 +171,7 @@
 
 <script>
 import {
-  getContractById,
+  getContractApprovalDetail,
   adaptContractFlowSignNodes,
   contractSigningStatusLabel,
   contractStatusLabel,
@@ -180,6 +180,9 @@ import {
 } from '@/api/hr';
 import { parseAttachmentIds } from '@/utils/attachment';
 
+/** 本合同详情组件只服务「劳动合同签订/续签审批」,businessKey 固定 */
+const CONTRACT_BUSINESS_KEY = 'hr_contract_approval';
+
 const CONTRACT_TYPES = [
   { value: 'FIXED', label: '固定期限劳动合同' },
   { value: 'NON_FIXED', label: '无固定期限劳动合同' },
@@ -266,8 +269,13 @@ function adaptContract(item = {}) {
 export default {
   name: 'HrContractDetail',
   props: {
+    /** 流程业务主键:签订/续签审批即合同主键 */
     businessId: {
       default: ''
+    },
+    /** 业务标识,默认取本组件对应的流程 */
+    businessKey: {
+      default: CONTRACT_BUSINESS_KEY
     }
   },
   data() {
@@ -299,8 +307,9 @@ export default {
       if (!id) return;
       this.loading = true;
       try {
-        const detail = await getContractById(id);
-        this.detailRow = adaptContract(detail || {});
+        const detail = await getContractApprovalDetail(id, this.businessKey);
+        if (!detail) throw new Error('未获取到合同详情,请后端确认审批详情的返回');
+        this.detailRow = adaptContract(detail);
       } catch (e) {
         this.detailRow = null;
         this.$message.error(e.message || '合同详情加载失败');

+ 26 - 12
src/views/bpm/handleTask/components/hrRemoveContractManagement/hrRemoveContractDetail.vue

@@ -70,10 +70,13 @@
 </template>
 
 <script>
-import { getContractById } from '@/api/hr';
+import { getContractApprovalDetail } from '@/api/hr';
 import { getByCode } from '@/api/system/dictionary-data';
 import { parseAttachmentIds } from '@/utils/attachment';
 
+/** 本组件只服务「劳动合同解除终止审批」,businessKey 固定 */
+const TERMINATION_BUSINESS_KEY = 'hr_contract_termination_approval';
+
 /** 业务类型:接口参数为 TERMINATE / END(见劳动合同解除终止接口) */
 const TERMINATION_TYPE_LABELS = {
   TERMINATE: '解除',
@@ -113,8 +116,13 @@ function pickTerminationFileIds(record = {}) {
 export default {
   name: 'HrRemoveContractDetail',
   props: {
+    /** 流程业务主键:解除终止审批即解除终止单主键(不是合同主键) */
     businessId: {
       default: ''
+    },
+    /** 业务标识,默认取本组件对应的流程 */
+    businessKey: {
+      default: TERMINATION_BUSINESS_KEY
     }
   },
   data() {
@@ -146,8 +154,11 @@ export default {
       // 原因编码字典单独拉取,失败不影响详情展示
       this.loadTerminationReasons();
       try {
-        const detail = await getContractById(id);
-        this.detailRow = this.adaptTermination(detail || {});
+        const contract = await getContractApprovalDetail(id, this.businessKey);
+        if (!contract) {
+          throw new Error('未获取到合同信息,请后端确认审批详情的返回');
+        }
+        this.detailRow = this.adaptTermination(contract, id);
       } catch (e) {
         this.detailRow = null;
         this.$message.error(e.message || '解除/终止详情加载失败');
@@ -156,19 +167,22 @@ export default {
       }
     },
     /**
-     * 合同详情接口返回该合同全部解除/终止记录(按创建时间倒序)。
-     * 审批单对应的是「最近一次解除终止单」terminationId,取不到时退回最新一条。
+     * 审批详情统一走 GET /hr/contracts/approval-detail:
+     * 服务端按 businessKey + businessId(解除终止单主键)解析出原合同,
+     * 返回合同完整档案(含 terminations 台账),前端按主键精确取本条解除单。
      */
-    adaptTermination(contract = {}) {
+    adaptTermination(contract = {}, terminationId = '') {
       const list = Array.isArray(contract.terminations)
         ? contract.terminations
         : [];
-      const record =
-        list.find(
-          (item) => String(item.id) === String(contract.terminationId)
-        ) ||
-        list[0] ||
-        {};
+      const record = list.find(
+        (item) => String(item.id) === String(terminationId)
+      );
+      if (!record) {
+        throw new Error(
+          `合同的解除终止台账中未找到解除单(id: ${terminationId}),请后端确认 approval-detail 是否返回该解除单`
+        );
+      }
       return {
         id: record.id ?? '',
         employeeName: contract.employeeName || '',