Selaa lähdekoodia

考勤规则请假打卡改走后端 leavePunch 枚举并支持关联假期制度、假期制度列表去硬编码假数据、假期制度操作列文案按状态区分、人脸识别与考勤机设备等未上线入口隐藏;入职办理表单项「状态」更正为「岗位性质」

lihaiting 5 päivää sitten
vanhempi
commit
25e92864e0

+ 41 - 5
src/api/hr/attendance.js

@@ -1065,7 +1065,8 @@ function buildPunchMethods(form) {
   }
   if (form.method === 'device' || form.method === 'phone-device') {
     push('ATTENDANCE_MACHINE');
-    if (form.faceRecognition === '启用' || form.faceEachPunch) push('FACE');
+    /* 人脸打卡未上线(助理管理里的人脸配置已隐藏),这里不再因 faceRecognition 追加 FACE;
+       恢复人脸功能时改回:if (form.faceRecognition === '启用' || form.faceEachPunch) push('FACE'); */
   }
   if (form.outsideEnabled) push('FIELD');
   if (form.relatedTarget === 'ding') push('PC');
@@ -1246,7 +1247,9 @@ export function serializeAttendanceRule(form = {}, extras = {}) {
       punchReminderMinutes: reminderMinutes(form.reminderOffset),
       outsidePolicy: OUTSIDE_POLICY[form.asstOutOfRange] || 'ALLOW_WITH_EXCEPTION',
       photoRequiredFlag: flag(form.photoEachPunch),
-      faceRequiredFlag: flag(form.faceEachPunch || form.faceRecognition === '启用'),
+      /* 人脸打卡未上线:助理管理里的人脸配置已隐藏,接口恒不启用;
+         恢复人脸时改回 flag(form.faceEachPunch || form.faceRecognition === '启用') */
+      faceRequiredFlag: 0,
       fieldSyncEnabled: flag(form.outsourceSync),
     },
     field: {
@@ -1261,6 +1264,18 @@ export function serializeAttendanceRule(form = {}, extras = {}) {
     },
     scopes,
     settlement: buildSettlement(form),
+    /* 请假离岗/返岗打卡设置:后端参数 leavePunch(两个字段都必填)
+       leavePunchRequired:0否、1是;punchTimeLimitType:见 LEAVE_PUNCH_TIME_OPTIONS 的枚举 */
+    leavePunch: {
+      leavePunchRequired: flag(form.leaveNeedPunch),
+      punchTimeLimitType: form.leavePunchWindow || 'UNLIMITED',
+    },
+    /* 该规则关联的启用假期制度主键(同一规则内请假类型不能重复,由页面勾选维护) */
+    leavePolicyIds: keepBigIntIdList(
+      (form.leavePolicyIds || []).map((item) =>
+        item && typeof item === 'object' ? item.id ?? item.policyId : item,
+      ),
+    ),
     exemptUserIds: keepBigIntIdList(
       form.exemptUserIds?.length
         ? form.exemptUserIds
@@ -1445,13 +1460,29 @@ export function adaptAttendanceRule(raw = {}) {
     outsidePhoto: on(field.photoRequiredFlag),
     reportStart: `${String(field.summaryHour ?? 9).padStart(2, '0')}:${String(field.summaryMinute ?? 0).padStart(2, '0')}`,
     reportEnd: '10:00',
-    leaveNeedPunch: true,
-    leavePunchWindow: '不限制',
+    /* 请假离岗/返岗打卡设置:读后端 leavePunch(回填真实值,不再硬编码)。
+       历史规则(后端字段上线前创建)没有该对象时才用后端默认:需打卡 + 不限制。 */
+    leaveNeedPunch: raw.leavePunch ? on(raw.leavePunch.leavePunchRequired) : true,
+    leavePunchWindow: raw.leavePunch?.punchTimeLimitType || 'UNLIMITED',
+    /* 该规则已关联的假期制度主键(页面「关联」勾选据此回显) */
+    leavePolicyIds: keepBigIntIdList(raw.leavePolicyIds || []),
     leaveRules: [],
     raw,
   };
 }
 
+/**
+ * 请假离岗/返岗「打卡时间」选项(value 就是后端 leavePunch.punchTimeLimitType 枚举)。
+ * 页面下拉必须用这套码,别再写中文字符串当 value(否则保存时对不上后端枚举)。
+ */
+export const LEAVE_PUNCH_TIME_OPTIONS = [
+  { value: 'UNLIMITED', label: '不限制' },
+  { value: 'BOTH_30_MINUTES', label: '上下班前后 30 分钟' },
+  { value: 'BOTH_60_MINUTES', label: '上下班前后 1 小时' },
+  { value: 'LEAVE_START_ONLY', label: '仅请假开始时间' },
+  { value: 'LEAVE_END_ONLY', label: '仅请假结束时间' },
+];
+
 const LEAVE_TYPE_TO_API = {
   年假: 'ANNUAL',
   事假: 'PERSONAL',
@@ -1526,7 +1557,12 @@ export function adaptLeavePolicy(raw = {}) {
     salary: extra.salary || extra.salaryRule || '',
     approval: extra.approval || (raw.status === 'ACTIVE' ? '已审核' : '待审核'),
     status: raw.status === 'ACTIVE' ? '已生效' : raw.status === 'DISABLED' ? '已归档' : '未生效',
-    action: raw.status === 'ACTIVE' || raw.status === 'DISABLED' ? '已归档' : '修改',
+    /* 操作列文案(点击后走真实的 saveLeavePolicy upsert 改状态):
+       DRAFT 未生效 → 修改(打开编辑弹窗)
+       ACTIVE 已生效 → 停用(改 DISABLED)
+       DISABLED 已归档 → 恢复(改回 DRAFT 可编辑)
+       ⚠ 原实现把 ACTIVE / DISABLED 都判成「已归档」,点已生效的行会走"恢复",是错的 */
+    action: raw.status === 'ACTIVE' ? '停用' : raw.status === 'DISABLED' ? '恢复' : '修改',
     effectiveStartDate: raw.effectiveStartDate ? String(raw.effectiveStartDate).slice(0, 10) : '',
     effectiveEndDate: raw.effectiveEndDate ? String(raw.effectiveEndDate).slice(0, 10) : '',
     remark: raw.remark || '',

+ 7 - 0
src/views/attendance/components/RuleAssistantPanel.vue

@@ -56,6 +56,10 @@
               </el-select>
             </el-form-item>
           </div>
+          <!--
+            拍照与人脸识别、每次打卡均需人脸识别 暂时隐藏:
+            平台当前只落地手机端 App 打卡,人脸识别未上线,接口不提交该配置。
+            恢复时放开以下两处注释,并同步恢复 api/hr/attendance.js 里 assistance.faceRequiredFlag 的计算。
           <el-form-item label="拍照与人脸识别">
             <el-select v-model="form.faceRecognition" placeholder="选择识别方式" style="width:100%" :disabled="readonly">
               <el-option label="启用" value="启用" />
@@ -63,6 +67,7 @@
               <el-option label="仅异常时启用" value="仅异常时启用" />
             </el-select>
           </el-form-item>
+          -->
         </template>
       </section>
 
@@ -75,9 +80,11 @@
           <el-form-item label="每次打卡均需拍照">
             <el-switch v-model="form.photoEachPunch" :disabled="readonly" />
           </el-form-item>
+          <!-- 每次打卡均需人脸识别:人脸未上线,暂时隐藏(恢复见上方说明)
           <el-form-item label="每次打卡均需人脸识别">
             <el-switch v-model="form.faceEachPunch" :disabled="readonly" />
           </el-form-item>
+          -->
         </section>
         <section class="form-section">
           <header>

+ 80 - 24
src/views/attendance/components/RuleLeavePanel.vue

@@ -10,12 +10,9 @@
           <el-switch v-model="form.leaveNeedPunch" active-text="开启" inactive-text="关闭" :disabled="readonly" />
         </el-form-item>
         <el-form-item label="打卡时间">
+          <!-- value 是后端 leavePunch.punchTimeLimitType 枚举,别再写中文字符串 -->
           <el-select v-model="form.leavePunchWindow" placeholder="选择打卡时间" style="width:240px" :disabled="readonly">
-            <el-option label="不限制" value="不限制" />
-            <el-option label="上下班前后 30 分钟" value="上下班前后 30 分钟" />
-            <el-option label="上下班前后 1 小时" value="上下班前后 1 小时" />
-            <el-option label="仅请假开始时间" value="仅请假开始时间" />
-            <el-option label="仅请假结束时间" value="仅请假结束时间" />
+            <el-option v-for="item in leavePunchTimeOptions" :key="item.value" :label="item.label" :value="item.value" />
           </el-select>
         </el-form-item>
       </section>
@@ -30,6 +27,13 @@
         </div>
         <el-table :data="form.leaveRules" class="leave-rule-table" size="small" border stripe>
           <el-table-column type="index" label="序号" width="60" align="center" header-align="center" />
+          <!-- 关联:勾选后随规则一起提交(leavePolicyIds);同一规则内请假类型不能重复 -->
+          <el-table-column label="关联" width="60" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <el-checkbox :value="isLeavePolicyLinked(row)" :disabled="readonly"
+                @change="(checked) => toggleLeavePolicyLink(row, checked)" />
+            </template>
+          </el-table-column>
           <el-table-column prop="category" label="假期分类" width="90" align="center" />
           <el-table-column prop="name" label="假期名称" width="100" align="center" />
           <el-table-column prop="type" label="假期类型" width="110" align="center" />
@@ -37,13 +41,6 @@
           <el-table-column prop="condition" label="满足条件" width="140" />
           <el-table-column prop="quota" label="年度额度" width="90" align="center" />
           <el-table-column prop="salary" label="薪资规则" width="130" />
-          <el-table-column prop="approval" label="审批流程" width="80" align="center">
-            <template slot-scope="{ row }">
-              <span class="status-pill" :class="row.approval === '已审核' ? 'success' : 'muted'">
-                <i></i>{{ row.approval }}
-              </span>
-            </template>
-          </el-table-column>
           <el-table-column prop="status" label="状态" width="80" align="center">
             <template slot-scope="{ row }">
               <span class="status-pill" :class="row.status === '已生效' ? 'success' : (row.status === '已归档' ? 'muted' : 'pending')">
@@ -53,7 +50,8 @@
           </el-table-column>
           <el-table-column v-if="!readonly" label="操作" width="90" align="left" fixed="right">
             <template slot-scope="{ row, $index }">
-              <el-button type="text" size="mini" :class="row.action === '已归档' ? 'is-archived' : ''" @click="onLeaveRuleAction(row, $index)">{{ row.action }}</el-button>
+              <el-button type="text" size="mini" :class="row.status === '已归档' ? 'is-archived' : ''"
+                :disabled="leaveSaving" @click="onLeaveRuleAction(row, $index)">{{ row.action }}</el-button>
             </template>
           </el-table-column>
         </el-table>
@@ -106,7 +104,7 @@
 </template>
 
 <script>
-import { saveLeavePolicy, serializeLeavePolicy, adaptLeavePolicy } from '@/api/hr/attendance';
+import { saveLeavePolicy, serializeLeavePolicy, adaptLeavePolicy, LEAVE_PUNCH_TIME_OPTIONS } from '@/api/hr/attendance';
 
 const leaveTypeOptions = [
   { label: '年假', value: 'ANNUAL' },
@@ -146,6 +144,8 @@ export default {
   data() {
     return {
       leaveTypeOptions,
+      /* 打卡时间下拉:value 即后端枚举(来自 api 层,别再本地写中文 value) */
+      leavePunchTimeOptions: LEAVE_PUNCH_TIME_OPTIONS,
       leaveDialogVisible: false,
       leaveDialogTitle: '新增假期制度',
       leaveSaving: false,
@@ -174,6 +174,7 @@ export default {
       this.$nextTick(() => this.$refs.leaveFormRef?.clearValidate?.());
     },
     async confirmLeaveDialog() {
+      if (this.leaveSaving) return;
       const valid = await new Promise((resolve) => {
         this.$refs.leaveFormRef?.validate((ok) => resolve(ok));
       });
@@ -216,19 +217,74 @@ export default {
         this.leaveSaving = false;
       }
     },
+    /**
+     * 该行是否已被本规则关联(form.leavePolicyIds 里是否有它的主键)。
+     * 勾选结果随规则一起提交(POST/PUT rules 的 leavePolicyIds)。
+     */
+    isLeavePolicyLinked(row) {
+      return (this.form.leavePolicyIds || []).some((id) => String(id) === String(row.id));
+    },
+    /**
+     * 勾选/取消关联。后端约定:同一规则内请假类型不能重复 → 勾选时前端先拦一次。
+     */
+    toggleLeavePolicyLink(row, checked) {
+      const ids = (this.form.leavePolicyIds || []).map(String);
+      if (!checked) {
+        this.$set(this.form, 'leavePolicyIds', ids.filter((id) => id !== String(row.id)));
+        return;
+      }
+      const duplicated = (this.form.leaveRules || []).find(
+        (item) => String(item.id) !== String(row.id)
+          && String(item.leaveType) === String(row.leaveType)
+          && ids.includes(String(item.id)),
+      );
+      if (duplicated) {
+        this.$message.warning(`同一规则内请假类型不能重复:「${row.type || row.leaveType}」已关联「${duplicated.name}」`);
+        return;
+      }
+      if (!ids.includes(String(row.id))) ids.push(String(row.id));
+      this.$set(this.form, 'leavePolicyIds', ids);
+    },
     onLeaveRuleAction(row, index) {
-      if (row.action === '已归档') {
-        this.$confirm('确定恢复该假期制度为可编辑?', '提示', { type: 'warning' })
-          .then(() => {
-            this.$set(this.form.leaveRules[index], 'action', '修改');
-            this.$set(this.form.leaveRules[index], 'approval', '待审核');
-            this.$set(this.form.leaveRules[index], 'status', '未生效');
-            this.$message.success('已恢复为可编辑');
-          })
-          .catch(() => {});
+      // 未生效 → 打开编辑弹窗;已生效 → 停用;已归档 → 恢复(后两者都要落库,见下)
+      if (row.action === '修改') {
+        this.openLeaveDialog(row);
         return;
       }
-      this.openLeaveDialog(row);
+      this.changeLeavePolicyStatus(row, index, row.action === '停用' ? '已归档' : '未生效');
+    },
+    /**
+     * 停用 / 恢复假期制度。
+     * ★ 原来只改本地内存(不发请求),刷新即还原 —— 现在走真实 upsert:
+     *   停用 → saveLeavePolicy(status=DISABLED);恢复 → saveLeavePolicy(status=DRAFT 即「未生效、可编辑」)。
+     * 确认框放在锁内(leaveSaving 先行置位),避免连点弹出两个确认框。
+     */
+    async changeLeavePolicyStatus(row, index, nextStatusText) {
+      if (this.leaveSaving) return;
+      const toDisabled = nextStatusText === '已归档';
+      this.leaveSaving = true;
+      let cancelled = false;
+      try {
+        await this.$confirm(
+          toDisabled ? '停用后该假期制度不能再被考勤规则关联,确定停用?' : '确定恢复该假期制度为可编辑(未生效)?',
+          '提示',
+          { type: 'warning' },
+        ).catch(() => {
+          cancelled = true;
+        });
+        if (cancelled) return;
+        const payload = serializeLeavePolicy({ ...row, status: nextStatusText });
+        await saveLeavePolicy(payload);
+        const next = toDisabled
+          ? { status: '已归档', action: '恢复' }
+          : { status: '未生效', action: '修改', approval: '待审核' };
+        Object.keys(next).forEach((key) => this.$set(this.form.leaveRules[index], key, next[key]));
+        this.$message.success(toDisabled ? '已停用' : '已恢复为可编辑');
+      } catch (error) {
+        this.$message.error(error.message || '操作失败');
+      } finally {
+        this.leaveSaving = false;
+      }
     },
   },
 };

+ 12 - 2
src/views/attendance/components/RuleMethodPanel.vue

@@ -32,13 +32,18 @@
       <section class="form-section">
         <header>
           <h3><span class="required">*</span>打卡方式</h3>
-          <p>手机和考勤机满足任意一项即可打卡</p>
+          <p>当前平台支持手机端 App 打卡</p>
         </header>
         <el-form-item label="选择方式" prop="method">
           <el-radio-group v-model="form.method" class="method-radio" :disabled="readonly">
             <el-radio label="phone">手机</el-radio>
+            <!--
+              考勤机 / 手机+考勤机 暂时隐藏:平台目前只落地「手机端 App 打卡」,
+              且考勤机设备侧接口不完整(无连接测试/同步/重启/日志/解绑),
+              恢复时放开下面两行,并同步放开本文件底部「考勤设备」区块。
             <el-radio label="device">考勤机</el-radio>
             <el-radio label="phone-device">手机+考勤机</el-radio>
+            -->
           </el-radio-group>
         </el-form-item>
       </section>
@@ -95,6 +100,11 @@
         </div>
       </section>
 
+      <!--
+        考勤设备暂时隐藏:平台当前只落地「手机端 App 打卡」,考勤机相关后端接口不完整
+        (pageDevices/saveDevice/updateDevice 之外,连接测试、同步数据、重启、日志、解绑均无接口,
+        原实现是前端定时器假成功 + 本地数组过滤),按约定不做假实现,先整块隐藏。
+        恢复时放开注释,并先补齐后端设备侧接口;原「点设备名称开详情」的入口一并恢复。
       <section v-if="form.method !== 'phone'" class="form-section">
         <header>
           <h3>考勤设备</h3>
@@ -106,7 +116,6 @@
           <el-table-column type="index" label="序号" width="60" align="center" />
           <el-table-column label="设备名称" minWidth="120">
             <template slot-scope="{ row }">
-              <!-- 点设备名称开详情,替代原操作列「查看详情」入口 -->
               <el-link v-if="row.name" type="primary" :underline="false" @click="$emit('device-view', row)">{{ row.name }}</el-link>
               <span v-else>{{ row.name }}</span>
             </template>
@@ -138,6 +147,7 @@
           </el-table-column>
         </el-table>
       </section>
+      -->
     </el-form>
   </div>
 </template>

+ 29 - 16
src/views/attendance/rule/index.vue

@@ -172,7 +172,12 @@
       </span>
     </ele-modal>
 
-    <!-- 新增/编辑考勤机 -->
+    <!--
+      考勤机设备管理暂时隐藏(原「新增/编辑考勤机」弹窗 + 原「设备详情 / 日志 共用查看弹窗」):
+      平台当前只落地手机端 App 打卡;且设备侧接口不完整(缺连接测试/同步数据/重启/日志/解绑),
+      原「连接测试/同步数据/重启设备」是前端定时器假成功、「日志」是前端现场编的、
+      「删除」只过滤本地数组不发请求。按「不做假实现」约定整块隐藏。
+      恢复时放开本区块,并先补齐后端设备侧接口,同步放开 components/RuleMethodPanel.vue 的考勤设备区块。
     <ele-modal :title="deviceDialogTitle" :visible.sync="deviceDialogVisible" width="520px" custom-class="modal"
       :close-on-click-modal="false" append-to-body>
       <el-form :model="deviceForm" :rules="deviceRules" ref="deviceFormRef" label-position="top">
@@ -204,7 +209,6 @@
       </span>
     </ele-modal>
 
-    <!-- 设备详情 / 日志 共用查看弹窗 -->
     <ele-modal :title="viewDialogTitle" :visible.sync="viewDialogVisible" width="520px" custom-class="modal"
       :close-on-click-modal="false" append-to-body>
       <div v-if="viewingDevice" class="device-view">
@@ -235,6 +239,7 @@
         <el-button @click="viewDialogVisible = false">关闭</el-button>
       </span>
     </ele-modal>
+    -->
   </div>
 </template>
 
@@ -496,22 +501,21 @@ export default {
         halfStart: '08:00',
         halfEnd: '12:00',
         leaveNeedPunch: true,
-        leavePunchWindow: '不限制',
-        leaveRules: [
-          { category: '请假', name: '年假', type: '带薪假', scope: '全体员工', condition: '在职年限≥1年', quota: '5天', salary: '不扣薪资', approval: '待审核', status: '未生效', action: '修改' },
-          { category: '请假', name: '事假', type: '扣除薪资', scope: '全体员工', condition: '入职就可', quota: '0天', salary: '按天扣除', approval: '已审核', status: '已生效', action: '已归档' },
-          { category: '请假', name: '病假', type: '带薪假', scope: '全体员工', condition: '入职就可', quota: '0.3天', salary: '按天扣除40%', approval: '已审核', status: '已生效', action: '已归档' },
-          { category: '请假', name: '婚假', type: '特殊假期', scope: '全体员工', condition: '入职就可', quota: '10天', salary: '不扣薪资', approval: '已审核', status: '已生效', action: '已归档' },
-          { category: '请假', name: '调休', type: '加班调休', scope: '全体员工', condition: '入职就可', quota: '0天', salary: '不扣薪资', approval: '已审核', status: '已生效', action: '已归档' },
-          { category: '请假', name: '产假', type: '特殊假期', scope: '转正员工', condition: '入职就可', quota: '25天', salary: '不扣薪资', approval: '已审核', status: '已生效', action: '已归档' }
-        ],
+        /* 请假打卡时间:value 是后端 leavePunch.punchTimeLimitType 枚举(见 LEAVE_PUNCH_TIME_OPTIONS) */
+        leavePunchWindow: 'UNLIMITED',
+        /* 假期制度列表与「本规则关联的制度」:
+           - leaveRules 只由接口 getLeavePolicyPage 填充(不再放硬编码假数据)
+           - leavePolicyIds 是本规则关联的启用制度主键,随规则一起提交 */
+        leaveRules: [],
+        leavePolicyIds: [],
         assistantEnabled: true,
         assistantExemptUsers: [],
         exemptUserIds: [],
         timezone: '(GMT+08:00)中国标准时间-北京',
         reminderOffset: '上班前 10 分钟、下班准点',
         asstOutOfRange: '记录为地点异常',
-        faceRecognition: '启用',
+        /* 人脸打卡未上线(助理管理里相关 UI 已隐藏),默认禁用且不提交 */
+        faceRecognition: '禁用',
         photoEachPunch: false,
         faceEachPunch: false,
         outsourceSync: false,
@@ -646,13 +650,16 @@ export default {
         return { list: [], count: 0 };
       }
     },
+    /** 拉取本租户的假期制度列表(页面表格「设置请假制度」的数据源)。
+     *  ★ 接口返回空就保持空(不再保留硬编码的 6 条假数据);请求失败也明确提示,
+     *    避免出现「看起来有制度、其实没落库」的假象。 */
     async loadLeavePolicies(form) {
       try {
-        const data = await getLeavePolicyPage({ pageNum: 1, size: 50 });
-        const list = (data.list || []).map(adaptLeavePolicy);
-        if (list.length) form.leaveRules = list;
+        const data = await getLeavePolicyPage({ pageNum: 1, size: 200 });
+        form.leaveRules = (data.list || []).map(adaptLeavePolicy);
       } catch (error) {
-        /* keep defaults */
+        form.leaveRules = [];
+        this.$message.error(error.message || '假期制度列表加载失败');
       }
     },
     async fillForm(row, readonly) {
@@ -810,6 +817,12 @@ export default {
         { success: '已停用:' + row.name, errorMessage: '停用失败' },
       );
     },
+    /* ===== 考勤机设备管理:当前无 UI 入口,已整块隐藏(平台只落地手机端 App 打卡) =====
+       - openDeviceCard / confirmDevice / editDevice / viewDevice / deleteDevice 走真实接口;
+       - testDevice / syncDevice / restartDevice 是前端定时器假成功,viewDeviceLog + buildMockLogs 是前端现场编日志,
+         deleteDevice 只从本地数组移除、不发解绑请求。
+       ★ 恢复「打卡方式 = 考勤机」与考勤设备区块前,必须先补齐后端设备侧接口
+         (连接测试 / 同步数据 / 重启 / 日志 / 解绑),禁止再保留假成功实现。 */
     openDeviceCard() {
       this.deviceForm = this.emptyDevice();
       this.editingDeviceIndex = -1;

+ 1 - 1
src/views/hrManagement/onboardingHandling/index.vue

@@ -16,7 +16,7 @@
             </el-form-item>
           </el-col>
           <el-col v-bind="styleResponsive ? { lg: 6, md: 12 } : { span: 6 }">
-            <el-form-item label="状态">
+            <el-form-item label="岗位性质">
               <el-select v-model="filters.positionType" clearable placeholder="请选择" style="width: 100%">
                 <el-option v-for="item in statusOptions" :key="String(item.value)" :label="item.label"
                   :value="item.value" />