员工排班功能全栈开发实践

927 阅读6分钟

项目背景

员工排班是一套ERP+POS系统内的功能,系统应用于一家连锁小商品店铺,店铺分为一家总店及多个分店,总店没有实体店,主要负责审核、财务等工作;分店是具体店铺,可以通过POS系统售卖货品。系统前端页面搭建采用HTML+SCSS语言,页面功能基于React框架+JSX实现,后端开发是Django框架+Python语言。

员工排班功能:各分店店长对其店铺员工每两周进行一次排班工作,每日排班分为三个时段,类型分为正常上班、公共假期、年假、休息等14种,排班信息保存后会按类型统计出两周内总工作时长。排班提交到总部后,总部财务人员会根据统计出来的总工时,结算员工工资。

页面功能实践

员工排班页面主要分为三大部分:

  • 筛选查询,包含开始结束时间、分店、员工筛选
  • 排班工时统计,按不同类型展示工时,并统计两周内总时长
  • 排班信息表,按员工展示两周内排班信息

复杂点在于筛选部分中开始时间的筛选,排班信息表的展示和时间段编辑。

image.png

1. 开始时间选项的获取

店长每两周一次的排班工作,首先选择想要编辑的排班对应的时间段,所以开始时间应该是周一的日期,为了能编辑以往未提交的排班,设定开始时间下拉选择内容为当前日期前后各8周的每周周一的日期,结束时间则是根据开始时间自动计算出的下一周周日的日期。

需要运用Date对象的getTime、getDay等方法以及一些js计算逻辑,因此让我对Date对象有了更多的理解,也更熟悉了其方法的运用。

image.png

开始时间获取思路:如何计算本周周一日期呢?

  • getTime方法可以返回1970年1月1日至今的毫秒数,由此获取今天的毫秒数
  • 计算今天距周一的天数,就能得到距周一的毫秒数
  • 二者相减即得到周一的毫秒数

有了基础的思路后,就能推算出前后各8周周一的毫秒数。再利用新建日期对象方法new Date(),传入毫秒数,就能将其转为对应的日期了;然后应用字符串split方法截取年、月、日,将日期转为显示格式。

将日期放入一个数组中,在select标签下直接循环数组,就可以实现筛选项的展示。

function getMondayDateArr() {
  let date = new Date()
  let dateTime = date.getTime()
  let weekDay = date.getDay() || 7  // getDay返回一周中的某一天,周日会返回0,因此将其记为7
  let oneDayTime = 24*60*60*1000
  let mondayDateArr = []
  for (let i = 0; i < 8; i++) {
    // 当天的周数减1可以得到今天距离周一的天数,因此可以通过加一个7的倍数,得出距前几周的周一的毫秒数
    let beforeMondayTime = dateTime - (weekDay - 1 + (i * 7)) * oneDayTime
    let beforeMondayDate = new Date(beforeMondayTime)
    let beforeLocalDate = beforeMondayDate.toLocaleDateString()
    // getFormatDate方法将日期格式转为显示格式,如2022-01-01
    let formatBeforeLocalDate = getFormatDate(beforeLocalDate)
    mondayDateArr.unshift(formatBeforeLocalDate)
    let afterMondayTime = dateTime + (7 - (weekDay - 1) + (i * 7)) * oneDayTime
    let afterMondayDate = new Date(afterMondayTime)
    let afterLocalDate = afterMondayDate.toLocaleDateString()
    let formatAfterLocalDate = getFormatDate(afterLocalDate)
    mondayDateArr.push(formatAfterLocalDate)
  }
  return mondayDateArr
}

// 日期选项数组的使用
<select>
  {
    mondayDateArr.map((mondayDate) => (
      <option value={mondayDate}>{mondayDate}</option>
    ))
  }
</select>
2. 排班信息表展示

排班信息表需要按各分店员工展示两周内每天排班信息,如果是新建排班或查询不到某个员工排班信息时,需要有一个默认排班表以供参考编辑。如何妥善展示这些信息很重要,因此我需要先设计好后端数据返回格式:在员工信息内包含其排班情况。前端发请求查询数据,查询不到的排班设为默认排班。页面展示时的多层嵌套循环以及循环内部的编辑修改让我对JSX语法更熟悉,积累了实现功能时前、后端总体设计编写经验。

image.png

后端数据库表设计及查询方式

数据库排班表:分为基础信息表及排班内容表,基础表记录开始时间,结束时间,分店等信息;内容表记录员工Id,排版类型,三个时段的开始结束时间等具体信息,两个表通过基础表Id关联。

查询:先通过时间、分店查询出基础表Id,再循环对应分店员工,根据员工Id和基础表Id,查询每日排班信息,按格式放入对应的员工信息中。返回的数据格式:

data: {
  startDate: '',
  endDate: '',
  branchID: '',
  status: '',
  content: [
    {staffID: 1, name: 'A', rosterInfo: [
      {date: '', weekDay: '', hours: '', type: '', periodOneStart: '', periodOneEnd: ''...}
      {date: '', weekDay: '', hours: '', type: '', periodOneStart: '', periodOneEnd: ''...}
      ...
    ]},
    {staffID: 2, name: 'B', rosterInfo: []}
    ...
  ]
}

前端查询展示

前端查询:发请求查询排班信息,如果查询不到某个员工或者整个时间段暂无排班信息(即上述数据中rosterInfo为空数组),就在前端将其设为默认排班信息。

排班表展示:先循环数据中content数组,展示员工姓名,再内部循环rosterInfo,将排班内容逐条展示

content.map((staff) => (
  <div>
    <h3>{staff.name}</h3>
    <table>
      <thead>
        <tr>
          <th>Week</th>
          <th>Date</th>
          <th>Type</th>
          <th>Period 1</th>
          <th>Hours</th>
        </tr>
      </thead>
      <tbody>
        {
          staff.rosterInfo.map((roster, index) => (
            <tr>
              <td>{roster.weekDay}</td>
              <td>{roster.date}</td>
              <td>{roster.type}</td>
              <td>{roster.periodOneStart + ‘-‘ + roster.periodOneEnd}</td>
              <td>{roster.hours}</td>
            </tr>
          ))
        }
      </tbody>
    </table>
  </div>
))
3. 排班表时间段编辑

排班表时间段编辑,选用弹窗方式,时间选择选项是从早9点到晚9点且以30分钟为间隔的时间选项,应用Date对象的setHours、getHours方法以及while循环,进一步熟悉Date对象,提高了逻辑思维能力。

image.png

如何获得半小时为间隔的时间筛选项呢?

  • 应用setHours和setMinutes方法设置一个初始时间,即早上9点
  • 利用while循环,只要小时数小于等于21点,则将时间放到时间选项数组里,并将分钟累加30。

只要更改时间间隔,就能获得以任意时间为间隔的时间数组啦。

由于while条件是小时数小于21,会将21:30分放入数组,所以用数组的pop方法,移除时间数组最后一个元素。

时间段选择好开始结束时间,就可以通过getTime方法进行加减计算,得出时间段长度,从而统计出每天总工作时长。

function getTimeOption() {
  let date = new Date()
  date.setHours(9)
  date.setMinutes(0)
  let timeOption = []
  while (date.getHours() <= 21) {
    let h = date.getHours()
    let m = date.getMinutes()
    timeOption.push(h + ':' + (m === 0 ? m + '0' : m))
    date.setMinutes(m + 30)
  }
  timeOption.pop()
  return timeOption
}

小结

通过员工排班的时间筛选和表内时间编辑,让我对Date对象更加熟悉,也能比较熟练的运用Date的各种方法处理各种时间计算情况;排班表设计展示,积累了前后端总体设计编写经验,提升了独立完成小项目的能力。 另外,我发现每个项目开始前都应该先拆分其功能点,避免功能堆积导致无从下手;拆分后对每个部分进行细化,编写前多思考,可以少走弯路,提升编写效率。