|
@@ -0,0 +1,233 @@
|
|
|
|
|
+const _ = require('lodash')
|
|
|
|
|
+const moment = require('moment-immutable')
|
|
|
|
|
+const { getWeeks, formatDate, parseDate } = require('../dates')
|
|
|
|
|
+const { StaffMember, Workday, Location, Labor, sequelize } = require('../database')
|
|
|
|
|
+const { Op } = require('sequelize')
|
|
|
|
|
+
|
|
|
|
|
+const list = async (req, res) => {
|
|
|
|
|
+ const locationKey = req.params.location
|
|
|
|
|
+ const location = await Location.findOne({where: {key: locationKey}})
|
|
|
|
|
+ if (!location) return res.status(404).end()
|
|
|
|
|
+ const workdays = await Workday.findAll({where: { locationId: location.id }})
|
|
|
|
|
+
|
|
|
|
|
+ let workweeks = _.groupBy(workdays, d => formatDate(moment(d.date).startOf('week')))
|
|
|
|
|
+
|
|
|
|
|
+ // fill workweeks
|
|
|
|
|
+ getWeeks(10).forEach(week => {
|
|
|
|
|
+ const w = formatDate(week)
|
|
|
|
|
+ if (!workweeks[w]) workweeks[w] = []
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // fill workdays
|
|
|
|
|
+ workweeks = _.chain(workweeks)
|
|
|
|
|
+ .toPairs()
|
|
|
|
|
+ .map(([w, days]) => {
|
|
|
|
|
+ days = _.chain(days)
|
|
|
|
|
+ .map(d => [moment(d.date).weekday(), d])
|
|
|
|
|
+ .fromPairs()
|
|
|
|
|
+ .value()
|
|
|
|
|
+
|
|
|
|
|
+ days = _.range(0, 7).map(d => days[d] || null)
|
|
|
|
|
+ .map(day => day && {
|
|
|
|
|
+ regularHours: day.regularHours,
|
|
|
|
|
+ overtimeHours: day.overtimeHours,
|
|
|
|
|
+ laborCost: day.laborCost
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ workweek: w,
|
|
|
|
|
+ workdays: days
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .sortBy(x => x.workweek)
|
|
|
|
|
+ .reverse()
|
|
|
|
|
+ .value()
|
|
|
|
|
+
|
|
|
|
|
+ res.status(200).send(workweeks)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const get = async (req, res) => {
|
|
|
|
|
+ const locationKey = req.params.location
|
|
|
|
|
+ const location = await Location.findOne({where: {key: locationKey}})
|
|
|
|
|
+ const week = parseDate(req.params.week)
|
|
|
|
|
+ const workdays = await Workday.findAll({
|
|
|
|
|
+ where: {
|
|
|
|
|
+ locationId: location.id,
|
|
|
|
|
+ date: {
|
|
|
|
|
+ [Op.gte]: week,
|
|
|
|
|
+ [Op.lt]: moment(week).endOf('week')
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ order: [ 'date' ]
|
|
|
|
|
+ })
|
|
|
|
|
+ const labor = await Labor.findAll({
|
|
|
|
|
+ where: {
|
|
|
|
|
+ workdayId: {
|
|
|
|
|
+ [Op.in]: workdays.map(x => x.id)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ const laborByWorkday = _.groupBy(labor, x => x.workdayId)
|
|
|
|
|
+ const extraStaffMembers = _.chain(labor)
|
|
|
|
|
+ .map(x => x.staffMemberId)
|
|
|
|
|
+ .filter(x => x)
|
|
|
|
|
+ .uniq()
|
|
|
|
|
+ .value()
|
|
|
|
|
+ const staffMembers = await StaffMember.findAll({
|
|
|
|
|
+ where: {
|
|
|
|
|
+ [Op.or]: [
|
|
|
|
|
+ { locationId: location.id },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: {
|
|
|
|
|
+ [Op.in]: extraStaffMembers
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ order: [ 'name' ]
|
|
|
|
|
+ })
|
|
|
|
|
+ const staffMembersById = _.chain(staffMembers).map(x => [x.id, x]).fromPairs().value()
|
|
|
|
|
+
|
|
|
|
|
+ // Fill in empty days
|
|
|
|
|
+ const workdaysByKey = _.chain(workdays).map(wd => [formatDate(wd.date), wd]).fromPairs().value()
|
|
|
|
|
+ const allWorkdays = []
|
|
|
|
|
+ for (let day = week, i = 0; i < 7; i++, day = day.add(1, 'day')) {
|
|
|
|
|
+ const wd = workdaysByKey[formatDate(day)]
|
|
|
|
|
+ allWorkdays[i] = wd ? wd.toJSON() : {}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const workdaysWithLabor = allWorkdays.map(wd => {
|
|
|
|
|
+ const labor = laborByWorkday[wd.id] || []
|
|
|
|
|
+ const laborByStaffMember = _.chain(labor).map(l => [l.staffMemberId, l]).fromPairs().value()
|
|
|
|
|
+ // Map from staffMembers to preserve sorting
|
|
|
|
|
+ wd.labor = staffMembers
|
|
|
|
|
+ .map(sm => laborByStaffMember[sm.id] || {staffMemberId: sm.id})
|
|
|
|
|
+ // Labor requires regularHours and overtimeHours, model requires hours
|
|
|
|
|
+ .map(sm => ({
|
|
|
|
|
+ staffMemberId: sm.staffMemberId,
|
|
|
|
|
+ hours: (sm.regularHours + sm.overtimeHours) || null
|
|
|
|
|
+ }))
|
|
|
|
|
+ // Restore any staffMembers that are no longer assigned this location
|
|
|
|
|
+ labor.forEach(l => {
|
|
|
|
|
+ if (!staffMembersById[l.staffMemberId]) {
|
|
|
|
|
+ wd.labor.push(l)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ return wd
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ res.status(200).send(allWorkdays)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const patch = async (req, res) => {
|
|
|
|
|
+ const transaction = await sequelize.transaction()
|
|
|
|
|
+ try {
|
|
|
|
|
+ const locationKey = req.params.location
|
|
|
|
|
+ const location = await Location.findOne({where: {key: locationKey}})
|
|
|
|
|
+ const week = parseDate(req.params.week)
|
|
|
|
|
+ const workdays = await Workday.findAll({
|
|
|
|
|
+ where: {
|
|
|
|
|
+ locationId: location.id,
|
|
|
|
|
+ date: {
|
|
|
|
|
+ [Op.gte]: week,
|
|
|
|
|
+ [Op.lt]: moment(week).endOf('week')
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ order: [ 'date' ]
|
|
|
|
|
+ })
|
|
|
|
|
+ const labor = await Labor.findAll({
|
|
|
|
|
+ where: {
|
|
|
|
|
+ workdayId: {
|
|
|
|
|
+ [Op.in]: workdays.map(x => x.id)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ const laborByWorkday = _.groupBy(labor, x => x.workdayId)
|
|
|
|
|
+ const extraStaffMembers = _.chain(labor)
|
|
|
|
|
+ .map(x => x.staffMemberId)
|
|
|
|
|
+ .filter(x => x)
|
|
|
|
|
+ .uniq()
|
|
|
|
|
+ .value()
|
|
|
|
|
+ const staffMembers = await StaffMember.findAll({
|
|
|
|
|
+ where: {
|
|
|
|
|
+ [Op.or]: [
|
|
|
|
|
+ { locationId: location.id },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: {
|
|
|
|
|
+ [Op.in]: extraStaffMembers
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ order: [ 'name' ]
|
|
|
|
|
+ })
|
|
|
|
|
+ const staffMembersById = _.chain(staffMembers).map(x => [x.id, x]).fromPairs().value()
|
|
|
|
|
+
|
|
|
|
|
+ // Fill in empty days
|
|
|
|
|
+ const workdaysByKey = _.chain(workdays).map(wd => [formatDate(wd.date), wd]).fromPairs().value()
|
|
|
|
|
+ const allWorkdays = []
|
|
|
|
|
+ for (let day = week, i = 0; i < 7; i++, day = day.add(1, 'day')) {
|
|
|
|
|
+ const wd = workdaysByKey[formatDate(day)]
|
|
|
|
|
+ allWorkdays[i] = wd || Workday.build({
|
|
|
|
|
+ locationId: location.id,
|
|
|
|
|
+ date: week.add(i, 'day')
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const workdaysWithLabor = allWorkdays.map(wd => {
|
|
|
|
|
+ const labor = laborByWorkday[wd.id] || []
|
|
|
|
|
+ const laborByStaffMember = _.chain(labor).map(l => [l.staffMemberId, l]).fromPairs().value()
|
|
|
|
|
+ // Map from staffMembers to preserve sorting
|
|
|
|
|
+ wd.labor = staffMembers.map(sm => laborByStaffMember[sm.id] || Labor.build({staffMemberId: sm.id, workdayId: wd.id}))
|
|
|
|
|
+ // Restore any staffMembers that are no longer assigned this location
|
|
|
|
|
+ labor.forEach(l => {
|
|
|
|
|
+ if (!staffMembersById[l.staffMemberId]) {
|
|
|
|
|
+ wd.labor.push(l)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ return wd
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // Update with model
|
|
|
|
|
+ const model = req.body
|
|
|
|
|
+ await Promise.all(model.map(async (modelWorkday, i) => {
|
|
|
|
|
+ const modelLaborById = _.chain(modelWorkday.labor).map(l => [l.staffMemberId, l]).fromPairs().value()
|
|
|
|
|
+ const workday = allWorkdays[i]
|
|
|
|
|
+ await Promise.all(workday.labor.map(async labor => {
|
|
|
|
|
+ const modelLabor = modelLaborById[labor.staffMemberId]
|
|
|
|
|
+ // modelLabor has hours, labor needs regularHours and overtimeHours
|
|
|
|
|
+ labor.regularHours = Math.min(8, modelLabor.hours) || 0
|
|
|
|
|
+ labor.overtimeHours = Math.max(0, modelLabor.hours - 8) || 0
|
|
|
|
|
+
|
|
|
|
|
+ if (labor.regularHours || labor.overtimeHours || !labor.isNewRecord) {
|
|
|
|
|
+ await labor.save({ transaction })
|
|
|
|
|
+ }
|
|
|
|
|
+ }))
|
|
|
|
|
+ // TODO: Update laborCost when wages are in
|
|
|
|
|
+ workday.overtimeHours = workday.labor.map(l => l.overtimeHours || 0).reduce((a, b) => a + b, 0)
|
|
|
|
|
+ workday.regularHours = workday.labor.map(l => l.regularHours || 0).reduce((a, b) => a + b, 0)
|
|
|
|
|
+ workday.laborCost = workday.labor.map(l => {
|
|
|
|
|
+ const staffMember = staffMembersById[l.staffMemberId]
|
|
|
|
|
+ const wage = staffMember.wage || 0
|
|
|
|
|
+ return ((l.regularHours || 0) * wage) + ((l.overtimeHours || 0) * wage * 1.5)
|
|
|
|
|
+ }).reduce((a, b) => a + b, 0)
|
|
|
|
|
+
|
|
|
|
|
+ if (workday.overtimeHours || workday.regularHours || !workday.isNewRecord) {
|
|
|
|
|
+ await workday.save({ transaction })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }))
|
|
|
|
|
+ await transaction.commit()
|
|
|
|
|
+
|
|
|
|
|
+ res.status(200).end()
|
|
|
|
|
+
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ await transaction.rollback()
|
|
|
|
|
+ throw err
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+module.exports = {
|
|
|
|
|
+ list,
|
|
|
|
|
+ get,
|
|
|
|
|
+ patch
|
|
|
|
|
+}
|