labor.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. const _ = require('lodash')
  2. const moment = require('moment-immutable')
  3. const { getWeeks, formatDate, parseDate } = require('../dates')
  4. const { StaffMember, Workday, Location, Labor, sequelize } = require('../database')
  5. const { Op } = require('sequelize')
  6. const list = async (req, res) => {
  7. const locationKey = req.params.location
  8. const location = await Location.findOne({where: {key: locationKey}})
  9. if (!location) return res.status(404).end()
  10. const workdays = await Workday.findAll({where: { locationId: location.id }})
  11. let workweeks = _.groupBy(workdays, d => formatDate(moment(d.date).startOf('week')))
  12. // fill workweeks
  13. getWeeks(10).forEach(week => {
  14. const w = formatDate(week)
  15. if (!workweeks[w]) workweeks[w] = []
  16. })
  17. // fill workdays
  18. workweeks = _.chain(workweeks)
  19. .toPairs()
  20. .map(([w, days]) => {
  21. days = _.chain(days)
  22. .map(d => [moment(d.date).weekday(), d])
  23. .fromPairs()
  24. .value()
  25. days = _.range(0, 7).map(d => days[d] || null)
  26. .map(day => day && {
  27. regularHours: day.regularHours,
  28. overtimeHours: day.overtimeHours,
  29. laborCost: day.laborCost
  30. })
  31. return {
  32. workweek: w,
  33. workdays: days
  34. }
  35. })
  36. .sortBy(x => x.workweek)
  37. .reverse()
  38. .value()
  39. res.status(200).send(workweeks)
  40. }
  41. const get = async (req, res) => {
  42. const locationKey = req.params.location
  43. const location = await Location.findOne({where: {key: locationKey}})
  44. const week = parseDate(req.params.week)
  45. const workdays = await Workday.findAll({
  46. where: {
  47. locationId: location.id,
  48. date: {
  49. [Op.gte]: week,
  50. [Op.lt]: moment(week).endOf('week')
  51. }
  52. },
  53. order: [ 'date' ]
  54. })
  55. const labor = await Labor.findAll({
  56. where: {
  57. workdayId: {
  58. [Op.in]: workdays.map(x => x.id)
  59. }
  60. }
  61. })
  62. const laborByWorkday = _.groupBy(labor, x => x.workdayId)
  63. const extraStaffMembers = _.chain(labor)
  64. .map(x => x.staffMemberId)
  65. .filter(x => x)
  66. .uniq()
  67. .value()
  68. const staffMembers = await StaffMember.findAll({
  69. where: {
  70. [Op.or]: [
  71. { locationId: location.id },
  72. {
  73. id: {
  74. [Op.in]: extraStaffMembers
  75. }
  76. }
  77. ]
  78. },
  79. order: [ 'name' ]
  80. })
  81. const staffMembersById = _.chain(staffMembers).map(x => [x.id, x]).fromPairs().value()
  82. // Fill in empty days
  83. const workdaysByKey = _.chain(workdays).map(wd => [formatDate(wd.date), wd]).fromPairs().value()
  84. const allWorkdays = []
  85. for (let day = week, i = 0; i < 7; i++, day = day.add(1, 'day')) {
  86. const wd = workdaysByKey[formatDate(day)]
  87. allWorkdays[i] = wd ? wd.toJSON() : {}
  88. }
  89. const workdaysWithLabor = allWorkdays.map(wd => {
  90. const labor = laborByWorkday[wd.id] || []
  91. const laborByStaffMember = _.chain(labor).map(l => [l.staffMemberId, l]).fromPairs().value()
  92. // Map from staffMembers to preserve sorting
  93. wd.labor = staffMembers
  94. .map(sm => laborByStaffMember[sm.id] || {staffMemberId: sm.id})
  95. // Labor requires regularHours and overtimeHours, model requires hours
  96. .map(sm => ({
  97. staffMemberId: sm.staffMemberId,
  98. hours: (sm.regularHours + sm.overtimeHours) || null
  99. }))
  100. // Restore any staffMembers that are no longer assigned this location
  101. labor.forEach(l => {
  102. if (!staffMembersById[l.staffMemberId]) {
  103. wd.labor.push(l)
  104. }
  105. })
  106. return wd
  107. })
  108. res.status(200).send(allWorkdays)
  109. }
  110. const patch = async (req, res) => {
  111. const transaction = await sequelize.transaction()
  112. try {
  113. const locationKey = req.params.location
  114. const location = await Location.findOne({where: {key: locationKey}})
  115. const week = parseDate(req.params.week)
  116. const workdays = await Workday.findAll({
  117. where: {
  118. locationId: location.id,
  119. date: {
  120. [Op.gte]: week,
  121. [Op.lt]: moment(week).endOf('week')
  122. }
  123. },
  124. order: [ 'date' ]
  125. })
  126. const labor = await Labor.findAll({
  127. where: {
  128. workdayId: {
  129. [Op.in]: workdays.map(x => x.id)
  130. }
  131. }
  132. })
  133. const laborByWorkday = _.groupBy(labor, x => x.workdayId)
  134. const extraStaffMembers = _.chain(labor)
  135. .map(x => x.staffMemberId)
  136. .filter(x => x)
  137. .uniq()
  138. .value()
  139. const staffMembers = await StaffMember.findAll({
  140. where: {
  141. [Op.or]: [
  142. { locationId: location.id },
  143. {
  144. id: {
  145. [Op.in]: extraStaffMembers
  146. }
  147. }
  148. ]
  149. },
  150. order: [ 'name' ]
  151. })
  152. const staffMembersById = _.chain(staffMembers).map(x => [x.id, x]).fromPairs().value()
  153. // Fill in empty days
  154. const workdaysByKey = _.chain(workdays).map(wd => [formatDate(wd.date), wd]).fromPairs().value()
  155. const allWorkdays = []
  156. for (let day = week, i = 0; i < 7; i++, day = day.add(1, 'day')) {
  157. const wd = workdaysByKey[formatDate(day)]
  158. allWorkdays[i] = wd || Workday.build({
  159. locationId: location.id,
  160. date: week.add(i, 'day')
  161. })
  162. }
  163. const workdaysWithLabor = allWorkdays.map(wd => {
  164. const labor = laborByWorkday[wd.id] || []
  165. const laborByStaffMember = _.chain(labor).map(l => [l.staffMemberId, l]).fromPairs().value()
  166. // Map from staffMembers to preserve sorting
  167. wd.labor = staffMembers.map(sm => laborByStaffMember[sm.id] || Labor.build({staffMemberId: sm.id, workdayId: wd.id}))
  168. // Restore any staffMembers that are no longer assigned this location
  169. labor.forEach(l => {
  170. if (!staffMembersById[l.staffMemberId]) {
  171. wd.labor.push(l)
  172. }
  173. })
  174. return wd
  175. })
  176. // Update with model
  177. const model = req.body
  178. await Promise.all(model.map(async (modelWorkday, i) => {
  179. const modelLaborById = _.chain(modelWorkday.labor).map(l => [l.staffMemberId, l]).fromPairs().value()
  180. const workday = allWorkdays[i]
  181. await Promise.all(workday.labor.map(async labor => {
  182. const modelLabor = modelLaborById[labor.staffMemberId]
  183. // modelLabor has hours, labor needs regularHours and overtimeHours
  184. labor.regularHours = Math.min(8, modelLabor.hours) || 0
  185. labor.overtimeHours = Math.max(0, modelLabor.hours - 8) || 0
  186. if (labor.regularHours || labor.overtimeHours || !labor.isNewRecord) {
  187. await labor.save({ transaction })
  188. }
  189. }))
  190. // TODO: Update laborCost when wages are in
  191. workday.overtimeHours = workday.labor.map(l => l.overtimeHours || 0).reduce((a, b) => a + b, 0)
  192. workday.regularHours = workday.labor.map(l => l.regularHours || 0).reduce((a, b) => a + b, 0)
  193. workday.laborCost = workday.labor.map(l => {
  194. const staffMember = staffMembersById[l.staffMemberId]
  195. const wage = staffMember.wage || 0
  196. return ((l.regularHours || 0) * wage) + ((l.overtimeHours || 0) * wage * 1.5)
  197. }).reduce((a, b) => a + b, 0)
  198. if (workday.overtimeHours || workday.regularHours || !workday.isNewRecord) {
  199. await workday.save({ transaction })
  200. }
  201. }))
  202. await transaction.commit()
  203. res.status(200).end()
  204. } catch (err) {
  205. await transaction.rollback()
  206. throw err
  207. }
  208. }
  209. module.exports = {
  210. list,
  211. get,
  212. patch
  213. }