Browse Source

转正申请员工下拉过滤已申请人员(含取消后重新编辑提交);员工档案直属上级改全员远程搜索并新增岗位性质/岗位层级必填校验

lihaiting 4 ngày trước cách đây
mục cha
commit
77d3bcfc3f

+ 80 - 34
src/views/hrManagement/employeeArchive/components/EmployeeArchiveForm.vue

@@ -132,23 +132,26 @@
                     </el-select>
                   </el-form-item>
                 </el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="岗位性质"><el-select v-model="form.employmentType" filterable
+                <el-col :xs="24" :sm="12"><el-form-item label="岗位性质" prop="employmentType"><el-select
+                      v-model="form.employmentType" filterable
                       clearable class="full-width" :placeholder="form.positionId ? '请选择岗位性质' : '请先选择岗位'"
                       @change="syncEmploymentTypeName">
                       <el-option v-for="item in employmentOptions" :key="item.key || item.value" :label="item.label"
                         :value="item.value" />
                     </el-select></el-form-item></el-col>
-                <el-col :xs="24" :sm="12"><el-form-item label="岗位层级"><el-select v-model="form.staffingLevel"
+                <el-col :xs="24" :sm="12"><el-form-item label="岗位层级" prop="staffingLevel"><el-select
+                      v-model="form.staffingLevel"
                       class="full-width" :disabled="!staffingLevelOptions.length"
                       :placeholder="form.positionId ? '请选择岗位层级' : '请先选择岗位'" @change="handleStaffingLevelChange">
                       <el-option v-for="item in staffingLevelOptions" :key="item" :label="item"
                         :value="item" /></el-select></el-form-item></el-col>
                 <el-col :xs="24" :sm="12">
                   <el-form-item label="直属上级">
-                    <el-select v-model="form.managerId" filterable clearable class="full-width"
-                      :loading="managerLoading" :disabled="!hasDept" :placeholder="hasDept ? '请选择直属上级' : '请先选择隶属部门'"
-                      @change="handleManagerChange">
-                      <el-option v-for="item in managerOptions" :key="item.id" :label="item.name" :value="item.id" />
+                    <el-select v-model="form.managerId" filterable remote clearable class="full-width"
+                      :loading="managerLoading" :remote-method="searchManagers" placeholder="输入姓名/工号搜索全员"
+                      @change="handleManagerChange" @focus="ensureManagerOptions">
+                      <el-option v-for="item in managerOptions" :key="item.id" :label="managerOptionLabel(item)"
+                        :value="item.id" />
                     </el-select>
                   </el-form-item>
                 </el-col>
@@ -639,7 +642,7 @@ import {
   getDepartmentPositionType,
   getDepartmentPositionLevels,
 } from '@/api/hr';
-import { listOrganizations, getFactoryAreaPage, getUserPage, EMPLOYEE_STATUS, EMPLOYEE_STATUS_CODE, CERT_TYPE } from '@/api/organization';
+import { listOrganizations, getFactoryAreaPage, searchUsersByKeyword, EMPLOYEE_STATUS, EMPLOYEE_STATUS_CODE, CERT_TYPE } from '@/api/organization';
 import { getDictByCode } from '@/api/sys';
 import { loadEmployeeStatusOptions } from '@/utils/employee-status-dict';
 /* 性别:接口口径 1 男 / 2 女(下拉 value 即接口码);没有值就留空,不提交该字段 */
@@ -651,6 +654,8 @@ import { positionAllowanceUnitOptions } from '@/enum/dict.js';
 import { getCertLevelOptions, isCertLevelDisabled, NO_LEVEL_CERT_TYPES, NO_LEVEL_VALUE } from '@/enum/certLevel';
 /* 所属机构默认值取登录缓存(loginChangeGroupVOList 第一条) */
 import { getCurrentOrgId, getOrgOptions } from '@/utils/current-org';
+/* 明确报错:岗位性质字典失败时提示,不静默留空 */
+import { showRequestError } from '@/utils/message';
 
 const emptyContract = () => ({ category: '', name: '', signDate: '', startDate: '', endDate: '', files: [], status: '生效', remark: '' });
 const emptyForm = () => ({
@@ -854,6 +859,38 @@ export default {
           },
           trigger: 'change',
         }],
+        /* 岗位性质(form.employmentType,提交字典 id):必填 */
+        employmentType: [{
+          required: true,
+          validator: (rule, value, callback) => {
+            if (this._skipFormValidate) return callback();
+            if (value == null || value === '') {
+              callback(new Error('请选择岗位性质'));
+              return;
+            }
+            callback();
+          },
+          trigger: 'change',
+        }],
+        /* 岗位层级(form.staffingLevel):必填。岗位未配置层级时下拉无候选,给出可操作的提示 */
+        staffingLevel: [{
+          required: true,
+          validator: (rule, value, callback) => {
+            if (this._skipFormValidate) return callback();
+            if (value == null || value === '') {
+              let message = '请选择岗位层级';
+              if (!this.staffingLevelOptions.length) {
+                message = this.form.positionId
+                  ? '当前岗位未配置岗位层级,请在岗位档案中维护后再保存'
+                  : '请先选择岗位';
+              }
+              callback(new Error(message));
+              return;
+            }
+            callback();
+          },
+          trigger: 'change',
+        }],
         phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }, { validator: validatePhone, trigger: 'blur' }],
         email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
         idCard: [{ validator: validateIdCard, trigger: ['blur', 'change'] }],
@@ -956,7 +993,8 @@ export default {
       return Boolean(this.form.positionSequenceId);
     },
     requiredProgress() {
-      const keys = ['name', 'employeeNo', 'gender', 'groupId', 'status', 'hireDate', 'positionId', 'phone'];
+      // 与 form rules 一一对应(含隶属部门 / 岗位序列 / 岗位性质 / 岗位层级),保证「必填 x/y」口径准确
+      const keys = ['name', 'employeeNo', 'gender', 'groupId', 'deptIds', 'status', 'hireDate', 'positionSequenceId', 'positionId', 'employmentType', 'staffingLevel', 'phone'];
       return { base: `${keys.filter((key) => { const value = this.form[key]; return Array.isArray(value) ? value.length : Boolean(value); }).length}/${keys.length}` };
     },
     sectionProgress() {
@@ -1175,11 +1213,13 @@ export default {
           // 部门岗位接口已给出层级下拉时,不再用岗位自带层级覆盖
           if (!this.staffingLevelOptions.length) this.syncStaffingLevelsFromPosition();
         });
+      /* 直属上级与部门/机构解耦(全员搜索):表单填充后预拉候选,
+         确保编辑时已有直属上级能立刻回显「名字(工号)」而不是裸 id */
+      this.searchManagers('');
       this.loadOrganizations().then(() => {
         /* 新增员工默认带出所属机构(登录机构列表第一个) */
         this.applyDefaultLegalEntity();
         this.normalizeDeptAgainstOptions();
-        this.loadManagerOptions({ keepSelection: true });
       });
       /* 所属工厂与机构、部门互不关联:并行加载,不等机构 */
       this.loadFactories().then(() => this.syncFactoryDisplay());
@@ -1739,7 +1779,7 @@ export default {
         .filter(Boolean)
         .join('、');
       if (resetDependents) {
-        this.loadManagerOptions({ keepSelection: false });
+        /* 直属上级不再随部门联动,这里只重置岗位 */
         this.resetPositionsByDepartment();
       }
     },
@@ -1776,17 +1816,15 @@ export default {
         input.setAttribute('placeholder', want);
       }
     },
-    /* 所属机构切换:只重置依赖机构的部门 / 岗位 / 汇报对象级联。
-       所属工厂与机构、部门互不关联(工厂列表是独立的平铺数据),这里保持已选工厂不动 */
+    /* 所属机构切换:只重置依赖机构的部门 / 岗位级联。
+       所属工厂与机构、部门互不关联(工厂列表是独立的平铺数据),这里保持已选工厂不动;
+       直属上级已改为全员搜索,与机构/部门解耦,同样保持已选直属上级不动 */
     handleLegalEntityChange(id) {
       const selected = this.legalEntities.find((item) => String(item.id) === String(id));
       this.form.legalEntity = selected?.name || '';
       this.form.company = selected?.name || this.form.company;
       this.deptCascaderKey += 1;
       this.applyDeptIds([]);
-      this.form.managerId = null;
-      this.form.manager = '';
-      this.managerOptions = [];
       this.resetPositionsByDepartment();
     },
     normalizeDeptAgainstOptions() {
@@ -1796,6 +1834,12 @@ export default {
       const valid = ids.filter((id) => this.findDeptNode(id));
       this.applyDeptIds(valid.length ? valid : ids);
     },
+    /** 直属上级下拉选项文案:名字(工号),无工号时只显示名字 */
+    managerOptionLabel(item = {}) {
+      const name = item.name || '未命名人员';
+      const jobNumber = item.jobNumber || item.employeeNo || '';
+      return jobNumber ? `${name}(${jobNumber})` : name;
+    },
     handleManagerChange(id) {
       const matched = this.managerOptions.find((item) => String(item.id) === String(id));
       this.form.managerId = id != null && id !== '' ? String(id) : null;
@@ -1808,27 +1852,23 @@ export default {
       if (exists) return;
       this.managerOptions.unshift({
         id: String(id),
-        name: this.form.manager || '未命名人员'
+        name: this.form.manager || '未命名人员',
+        jobNumber: ''
       });
     },
-    async loadManagerOptions({ keepSelection = false } = {}) {
-      const deptId = this.form.deptId;
-      if (!keepSelection) {
-        this.form.managerId = null;
-        this.form.manager = '';
-      }
-      if (!deptId) {
-        this.managerOptions = [];
-        return;
-      }
-      const requestKey = String(deptId);
+    /**
+     * 直属上级:远程搜索全员(姓名/工号/手机号,searchUsersByKeyword 内部并行检索),
+     * 不再按隶属部门过滤——新员工尚未落部门或部门下无人时也能直接选人;
+     * 登录人组织范围由 getUserPage 自身权限控制,isQueryLZ=0 不带离职员工。
+     */
+    async searchManagers(keyword) {
+      const requestKey = String(keyword || '');
       this._managerRequestKey = requestKey;
       this.managerLoading = true;
       try {
-        const data = await getUserPage({
+        const data = await searchUsersByKeyword(keyword || '', {
           pageNum: 1,
-          size: 99999,
-          groupId: deptId,
+          size: 30,
           isQueryLZ: 0
         });
         if (this._managerRequestKey !== requestKey) return;
@@ -1836,12 +1876,10 @@ export default {
         this.managerOptions = (data?.list || [])
           .map((item) => ({
             id: item.id == null ? item.id : String(item.id),
-            name: item.name || ''
+            name: item.name || '',
+            jobNumber: item.jobNumber || item.employeeNo || ''
           }))
           .filter((item) => item.id != null && item.id !== '' && item.name && String(item.id) !== selfId);
-        if (this.form.managerId != null && this.form.managerId !== '') {
-          this.form.managerId = String(this.form.managerId);
-        }
         this.mergeCurrentManagerOption();
       } catch (error) {
         if (this._managerRequestKey !== requestKey) return;
@@ -1853,6 +1891,12 @@ export default {
         }
       }
     },
+    /** 首次点开下拉时候选还是空的,补拉一次默认列表 */
+    ensureManagerOptions() {
+      if (!this.managerOptions.length && !this.managerLoading) {
+        this.searchManagers('');
+      }
+    },
     async loadOrganizations() {
       try {
         const list = await listOrganizations();
@@ -2209,7 +2253,9 @@ export default {
           this.syncEmploymentTypeName(this.form.employmentType);
         }
       } catch (error) {
+        // 岗位性质已改为必填:字典取不到就明确报错,不要静默留空导致下拉无选项、用户不知道原因
         this.employmentOptions = [];
+        showRequestError(error, '岗位性质字典加载失败,请刷新重试');
       }
     },
     /** 接口返回可能是数组 / {list|records|rows|data},统一取数组(与办理入职一致) */

+ 27 - 4
src/views/hrManagement/regularizationHandling/index.vue

@@ -656,14 +656,37 @@ export default {
         },
       ];
     },
+    /**
+     * 转正申请的「申请员工」下拉:应为**尚未转正(没有转正单)的试用期员工**。
+     * 数据源 trialEmployees 只按 状态=试用期/未离职 拉全量,本身不含「是否已有转正单」的排除,
+     * 故这里复用 loadBillUserIds(与「待申请」页签同一份「已有转正单」的员工集合)把已申请的人剔除。
+     * ⚠ 编辑/查看已存在单据时,必须把当前申请人保留在选项里,否则 el-select 回显不出 label。
+     */
     selectableEmployees() {
-      return this.trialEmployees;
+      const employees = this.trialEmployees;
+      const bill = this.billUserIds;
+      if (!bill) return employees;
+      const filtered = employees.filter(
+        (item) => !bill.has(String(item.userId)),
+      );
+      const current = this.form.userId;
+      if (!current) return filtered;
+      if (filtered.some((item) => String(item.userId) === String(current))) {
+        return filtered;
+      }
+      const currentEmp = employees.find(
+        (item) => String(item.userId) === String(current),
+      );
+      return currentEmp ? [...filtered, currentEmp] : filtered;
     },
   },
   created() {
     // 表格挂载后会自动加载第一页(datasource: fetchPage)
     this.loadTemplates();
     this.loadEmployees();
+    // 预拉「已有转正单的员工集合」:申请员工下拉需要它来剔除已转正/已申请的人
+    // (与「待申请」页签共用,避免弹窗打开时下拉还漏过滤)
+    this.loadBillUserIds();
     this.loadOrganizations();
     this.loadPositionNatureOptions();
   },
@@ -695,14 +718,14 @@ export default {
         row.status,
       );
     },
-    /** 可提交审批:草稿/已撤回/已驳回 + 有转正单主键 */
+    /** 可提交审批:草稿/已撤回/已驳回/已取消 + 有转正单主键(已取消后可重新编辑发起) */
     canEdit(row) {
-      return Boolean(row.regularizationId) && ["draft", "withdrawn", "rejected"].includes(row.status);
+      return Boolean(row.regularizationId) && ["draft", "withdrawn", "rejected", "cancelled"].includes(row.status);
     },
     canSubmit(row) {
       return (
         row.regularizationId &&
-        ["draft", "withdrawn", "rejected"].includes(row.status)
+        ["draft", "withdrawn", "rejected", "cancelled"].includes(row.status)
       );
     },
     /** 可查看审批记录:必须有有效的审批实例 id(approvalInstanceId / processInstanceId) */