huminmin
3 天以前 28fc65c34d1642b007dc45b83782e23be58d718c
src/views/personnelManagement/classsSheduling/index.vue
@@ -49,13 +49,11 @@
          <div class="search-actions">
            <el-button size="small"
                       type="primary"
                       @click="refreshTable()"
                       :icon="Search">
                       @click="refreshTable()">
              查询
            </el-button>
            <el-button size="small"
                       @click="refresh()"
                       :icon="Refresh"
                       style="margin-left: 8px">
              重置
            </el-button>
@@ -63,22 +61,19 @@
          <div class="search-buttons">
            <el-button size="small"
                       type="primary"
                       @click="configTime"
                       :icon="Setting">
                       @click="configTime">
              班次配置
            </el-button>
            <el-button size="small"
                       type="success"
                       @click="handleDown"
                       :loading="downLoading"
                       :icon="Download"
                       style="margin-left: 8px">
              导出
            </el-button>
            <el-button size="small"
                       type="warning"
                       @click="schedulingVisible = true"
                       :icon="Calendar"
                       style="margin-left: 8px">
              排班
            </el-button>
@@ -147,19 +142,20 @@
                     :key="'d' + i">
                  <el-dropdown trigger="click"
                               placement="bottom"
                               @command="(e) => handleCommand(e, m)"
                               :disabled="isCellBlockedByLeaveDate(item, m)"
                               @command="(e) => handleCommand(e, item, m)"
                               class="shift-dropdown">
                    <div class="shift-box"
                         :class="{
                      'shift-box-early': m.shift === '早班',
                      'shift-box-mid': m.shift === '中班',
                      'shift-box-night': m.shift === '夜班',
                      'shift-box-rest': m.shift === '休息',
                      'shift-box-rest': !m.shift || m.shift === '休息',
                      'shift-box-leave': m.shift === '请假',
                      'shift-box-other': m.shift === '夜11',
                      'shift-box-business': m.shift === '夜12',
                    }">
                      <span class="shift-text">{{ getShiftNameByValue(m.shift) || '—' }}</span>
                      <span class="shift-text">{{ getShiftNameByValue(m.shift) || '休息' }}</span>
                    </div>
                    <template #dropdown>
                      <el-dropdown-menu>
@@ -204,7 +200,7 @@
              </div> -->
              <div class="user-total">
                <span class="total-label">合计出勤:</span>
                <span class="total-value">{{ item.work_time }}天</span>
                <span class="total-value">{{ item.year.totalYearAttendance }}天</span>
              </div>
            </div>
          </div>
@@ -276,6 +272,23 @@
      <div class="search_thing">
        <div class="search_label"
             style="width: 90px">
          <span style="color: red; margin-right: 4px">*</span>部门:
        </div>
        <div class="search_input">
          <el-tree-select v-model="schedulingQuery.deptId"
                          :data="deptOptions"
                          :props="{ value: 'id', label: 'label', children: 'children' }"
                          value-key="id"
                          placeholder="请选择部门"
                          check-strictly
                          clearable
                          @change="onSchedulingDeptChange"
                          style="width: 100%" />
        </div>
      </div>
      <div class="search_thing">
        <div class="search_label"
             style="width: 90px">
          <span style="color: red; margin-right: 4px">*</span>人员名称:
        </div>
        <div class="search_input">
@@ -284,7 +297,8 @@
                     style="width: 100%"
                     multiple
                     clearable
                     collapse-tags>
                     collapse-tags
                     :disabled="!schedulingQuery.deptId">
            <el-option v-for="item in personList"
                       :key="item.id"
                       :label="item.staffName"
@@ -301,8 +315,9 @@
        <div class="search_input">
          <el-select v-model="schedulingQuery.shift"
                     placeholder="请选择"
                     style="width: 100%">
            <el-option v-for="item in classType"
                     style="width: 100%"
                     :disabled="!schedulingQuery.deptId">
            <el-option v-for="item in schedulingClassType"
                       :key="item.id"
                       :label="getShiftNameByValue(item.shift)"
                       :value="item.id">
@@ -323,7 +338,14 @@
</template>
<script setup>
  import { ref, reactive, onMounted, getCurrentInstance } from "vue";
  import {
    ref,
    reactive,
    computed,
    watch,
    onMounted,
    getCurrentInstance,
  } from "vue";
  import { useRouter } from "vue-router";
  import {
    page,
@@ -333,6 +355,7 @@
    update,
    staffOnJobListPage,
  } from "@/api/personnelManagement/class";
  import { findStaffLeaveListPage } from "@/api/personnelManagement/staffLeave.js";
  import { deptTreeSelect } from "@/api/system/user.js";
  import { getAttendanceRules } from "@/api/personnelManagement/attendanceRules.js";
  import { useDict } from "@/utils/dict";
@@ -381,6 +404,8 @@
  // 人员列表
  const personList = ref([]);
  const personListAll = ref([]);
  const leaveDateMap = ref({});
  // 加载状态
  const loading = ref(false);
@@ -388,8 +413,18 @@
  // 排班查询条件
  const schedulingQuery = reactive({
    week: "",
    deptId: "",
    userId: null,
    shift: "",
  });
  const schedulingClassType = computed(() => {
    if (!schedulingQuery.deptId) return classType.value || [];
    return (classType.value || []).filter(item => {
      if (["998", "999"].includes(String(item?.id))) return true;
      const itemDeptId = item.deptId ?? item.sysDeptId ?? item.dept_id;
      return String(itemDeptId) === String(schedulingQuery.deptId);
    });
  });
  // 列表数据
@@ -598,6 +633,10 @@
      proxy.$modal.msgError("请选择周次");
      return;
    }
    if (!schedulingQuery.deptId) {
      proxy.$modal.msgError("请选择部门");
      return;
    }
    let time = schedulingQuery.week.getTime();
    // 格式化日期为 YYYY-MM-DD 格式
@@ -621,6 +660,15 @@
      proxy.$modal.msgError("请选择班次");
      return;
    }
    const blockedEmployees = (personList.value || []).filter(item => {
      const leaveDate = getEmployeeLeaveDate(item?.id);
      const weekEndDate = getSchedulingWeekEndDate();
      return leaveDate && weekEndDate && weekEndDate.getTime() > leaveDate.getTime();
    });
    if (blockedEmployees.length) {
      proxy.$modal.msgError("存在已超过离职日期的人员,不能继续排班");
      return;
    }
    loading.value = true;
    add({
      startWeek,
@@ -633,8 +681,10 @@
        proxy.$modal.msgSuccess("操作成功");
        schedulingVisible.value = false;
        schedulingQuery.week = "";
        schedulingQuery.deptId = "";
        schedulingQuery.userId = null;
        schedulingQuery.shift = "";
        personList.value = [];
        refresh();
      })
      .catch(err => {
@@ -674,10 +724,12 @@
    })
      .then(res => {
        proxy.$modal.msgSuccess("下载成功");
        downLoading.value = false;
        const blob = new Blob([res], {
          type: "application/force-download",
        });
        const blob =
          res instanceof Blob
            ? res
            : new Blob([res], {
                type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
              });
        let fileName = "";
        if (query.month) {
          fileName = year + "-" + query.month + " 班次信息";
@@ -686,15 +738,20 @@
        }
        proxy.$download.saveAs(blob, fileName + ".xlsx");
      })
      .catch(err => {
      .catch(err => {})
      .finally(() => {
        downLoading.value = false;
      });
  };
  // 处理命令
  const handleCommand = (e, m) => {
  const handleCommand = (e, row, cell) => {
    if (isCellBlockedByLeaveDate(row, cell)) {
      proxy.$modal.msgError("该员工已超出离职日期,不能继续排班");
      return;
    }
    // if (e != m.shift) {
    update({
      id: m.id,
      id: cell.id,
      personalAttendanceLocationConfigId: e,
    }).then(res => {
      proxy.$modal.msgSuccess("操作成功");
@@ -710,7 +767,18 @@
  // 查询规则列表
  const fetchData = () => {
    getAttendanceRules({ current: -1, size: -1 }).then(res => {
      classType.value = res.data.records;
      const records = Array.isArray(res?.data?.records) ? res.data.records : [];
      const appendList = [
        { id: 998, shift: "休息" },
        { id: 999, shift: "请假" },
      ];
      const exists = new Set(records.map(r => String(r?.id)));
      appendList.forEach(item => {
        if (!exists.has(String(item.id))) {
          records.push(item);
        }
      });
      classType.value = records;
    });
  };
  // 获取用户
@@ -725,8 +793,139 @@
      staffState: 1,
    }).then(res => {
      let arr = res.data.records;
      personList.value = arr;
      personListAll.value = arr;
      updatePersonListByDept();
    });
  };
  const getLeaveDates = () => {
    findStaffLeaveListPage({
      current: -1,
      size: -1,
    }).then(res => {
      const records = Array.isArray(res?.data?.records) ? res.data.records : [];
      const map = {};
      records.forEach(item => {
        const staffId = item?.staffOnJobId ?? item?.staff_on_job_id;
        const leaveDate = item?.leaveDate ?? item?.leave_date;
        if (!staffId || !leaveDate) return;
        const key = String(staffId);
        const prev = map[key];
        if (!prev || String(leaveDate) > String(prev)) {
          map[key] = leaveDate;
        }
      });
      leaveDateMap.value = map;
      updatePersonListByDept();
      syncPersonListSelection();
    });
  };
  const findDeptNodeById = (deptId, deptList = deptOptions.value) => {
    const list = Array.isArray(deptList) ? deptList : [];
    for (const node of list) {
      if (String(node.id) === String(deptId)) return node;
      if (node.children && node.children.length) {
        const found = findDeptNodeById(deptId, node.children);
        if (found) return found;
      }
    }
    return null;
  };
  const collectDeptIds = node => {
    const ids = [];
    if (!node) return ids;
    ids.push(String(node.id));
    if (node.children && node.children.length) {
      node.children.forEach(child => {
        ids.push(...collectDeptIds(child));
      });
    }
    return ids;
  };
  const updatePersonListByDept = () => {
    if (!schedulingQuery.deptId) {
      personList.value = [];
      return;
    }
    const node = findDeptNodeById(schedulingQuery.deptId);
    const deptIds = new Set(collectDeptIds(node));
    personList.value = (personListAll.value || []).filter(item => {
      const itemDeptId = item.deptId ?? item.sysDeptId ?? item.dept_id;
      return deptIds.has(String(itemDeptId)) && !isEmployeeBlockedByLeaveDate(item);
    });
  };
  const parseDateOnly = value => {
    if (!value) return null;
    if (value instanceof Date) {
      const d = new Date(value);
      d.setHours(0, 0, 0, 0);
      return d;
    }
    const normalized = String(value).trim().slice(0, 10);
    if (!normalized) return null;
    const d = new Date(normalized.replace(/-/g, "/"));
    if (Number.isNaN(d.getTime())) return null;
    d.setHours(0, 0, 0, 0);
    return d;
  };
  const getSchedulingWeekEndDate = () => {
    if (!schedulingQuery.week) return null;
    const start = new Date(schedulingQuery.week);
    start.setHours(0, 0, 0, 0);
    return new Date(start.getTime() + 24 * 60 * 60 * 1000 * 5);
  };
  const getCellDate = cell => parseDateOnly(cell?.time);
  const getEmployeeLeaveDate = staffId => {
    const leaveDate = leaveDateMap.value[String(staffId)];
    return parseDateOnly(leaveDate);
  };
  const isEmployeeBlockedByLeaveDate = item => {
    const leaveDate = getEmployeeLeaveDate(item?.id);
    if (!leaveDate) return false;
    const weekEndDate = getSchedulingWeekEndDate();
    if (!weekEndDate) return false;
    return weekEndDate.getTime() > leaveDate.getTime();
  };
  const isCellBlockedByLeaveDate = (row, cell) => {
    const staffId = row?.userId ?? row?.user_id ?? row?.id;
    const leaveDate = getEmployeeLeaveDate(staffId);
    const cellDate = getCellDate(cell);
    if (!leaveDate || !cellDate) return false;
    return cellDate.getTime() > leaveDate.getTime();
  };
  const syncPersonListSelection = () => {
    if (!Array.isArray(schedulingQuery.userId) || !schedulingQuery.userId.length) return;
    const validIds = new Set((personList.value || []).map(item => String(item.id)));
    const nextIds = schedulingQuery.userId.filter(id => validIds.has(String(id)));
    if (nextIds.length !== schedulingQuery.userId.length) {
      schedulingQuery.userId = nextIds;
      proxy.$modal.msgWarning("已自动剔除超出离职日期的人员");
    }
  };
  const setDefaultShiftByDept = () => {
    if (!schedulingQuery.deptId) {
      schedulingQuery.shift = "";
      return;
    }
    const first = schedulingClassType.value?.[0];
    schedulingQuery.shift = first ? first.id : "";
  };
  const onSchedulingDeptChange = () => {
    schedulingQuery.userId = null;
    updatePersonListByDept();
    setDefaultShiftByDept();
  };
  // 根据字典获取日期
@@ -750,6 +949,7 @@
  onMounted(() => {
    fetchData();
    getUsers();
    getLeaveDates();
    fetchDeptOptions();
    if (query.month) {
      init();
@@ -762,6 +962,45 @@
    }
    monthList.value.reverse();
  });
  watch(
    () => classType.value,
    () => {
      if (schedulingVisible.value && schedulingQuery.deptId) {
        setDefaultShiftByDept();
      }
    }
  );
  watch(
    () => schedulingQuery.week,
    () => {
      if (!schedulingVisible.value) return;
      updatePersonListByDept();
      syncPersonListSelection();
    }
  );
  watch(
    () => schedulingQuery.deptId,
    () => {
      if (!schedulingVisible.value) return;
      updatePersonListByDept();
      syncPersonListSelection();
    }
  );
  watch(
    () => schedulingVisible.value,
    val => {
      if (!val) return;
      schedulingQuery.week = "";
      schedulingQuery.deptId = "";
      schedulingQuery.userId = null;
      schedulingQuery.shift = "";
      personList.value = [];
    }
  );
</script>
<style scoped>
@@ -974,7 +1213,7 @@
  .user-stats {
    /* display: flex; */
    /* flex-wrap: wrap;
                                                                                                                                                                                                                                                                                    gap: 10px; */
                                                                                                                                                                                                                                                                                                      gap: 10px; */
    margin-bottom: 4px;
  }
@@ -1224,6 +1463,7 @@
    display: flex;
    flex-direction: column;
    align-items: center;
    height: 65px;
  }
  /* 月度出勤 */