labor.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. const _ = require('lodash')
  2. const moment = require('moment-immutable')
  3. const { getWeeks, formatDate, parseDate } = require('../dates')
  4. const { StaffMember, Workday, Terminal, Labor, sequelize } = require('../database')
  5. const { Op } = require('sequelize')
  6. const list = async (req, res) => {
  7. const terminalKey = req.params.terminal
  8. const terminal = await Terminal.findOne({where: {key: terminalKey}})
  9. if (!terminal) return res.status(404).end()
  10. const workdays = await Workday.findAll({where: { terminalId: terminal.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 terminalKey = req.params.terminal
  43. const terminal = await Terminal.findOne({where: {key: terminalKey}})
  44. const week = parseDate(req.params.week)
  45. const workdays = await Workday.findAll({
  46. where: {
  47. terminalId: terminal.id,
  48. date: {
  49. [Op.gte]: week,
  50. [Op.lte]: 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. { terminalId: terminal.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 terminal
  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({
  109. workdays: allWorkdays
  110. })
  111. }
  112. const patch = async (req, res) => {
  113. const transaction = await sequelize.transaction()
  114. try {
  115. const terminalKey = req.params.terminal
  116. const terminal = await Terminal.findOne({where: {key: terminalKey}})
  117. const week = parseDate(req.params.week)
  118. const workdays = await Workday.findAll({
  119. where: {
  120. terminalId: terminal.id,
  121. date: {
  122. [Op.gte]: week,
  123. [Op.lt]: moment(week).endOf('week')
  124. }
  125. },
  126. order: [ 'date' ]
  127. })
  128. const labor = await Labor.findAll({
  129. where: {
  130. workdayId: {
  131. [Op.in]: workdays.map(x => x.id)
  132. }
  133. }
  134. })
  135. const laborByWorkday = _.groupBy(labor, x => x.workdayId)
  136. const extraStaffMembers = _.chain(labor)
  137. .map(x => x.staffMemberId)
  138. .filter(x => x)
  139. .uniq()
  140. .value()
  141. const staffMembers = await StaffMember.findAll({
  142. where: {
  143. [Op.or]: [
  144. { terminalId: terminal.id },
  145. {
  146. id: {
  147. [Op.in]: extraStaffMembers
  148. }
  149. }
  150. ]
  151. },
  152. order: [ 'name' ]
  153. })
  154. const staffMembersById = _.chain(staffMembers).map(x => [x.id, x]).fromPairs().value()
  155. // Fill in empty days
  156. const workdaysByKey = _.chain(workdays).map(wd => [formatDate(wd.date), wd]).fromPairs().value()
  157. const allWorkdays = []
  158. for (let day = week, i = 0; i < 7; i++, day = day.add(1, 'day')) {
  159. const wd = workdaysByKey[formatDate(day)]
  160. allWorkdays[i] = wd || Workday.build({
  161. terminalId: terminal.id,
  162. date: week.add(i, 'day')
  163. })
  164. }
  165. const workdaysWithLabor = allWorkdays.map(wd => {
  166. const labor = laborByWorkday[wd.id] || []
  167. const laborByStaffMember = _.chain(labor).map(l => [l.staffMemberId, l]).fromPairs().value()
  168. // Map from staffMembers to preserve sorting
  169. wd.labor = staffMembers.map(sm => laborByStaffMember[sm.id] || Labor.build({staffMemberId: sm.id, workdayId: wd.id}))
  170. // Restore any staffMembers that are no longer assigned this terminal
  171. labor.forEach(l => {
  172. if (!staffMembersById[l.staffMemberId]) {
  173. wd.labor.push(l)
  174. }
  175. })
  176. return wd
  177. })
  178. // Update with model
  179. const model = req.body
  180. await Promise.all(model.workdays.map(async (modelWorkday, i) => {
  181. const modelLaborById = _.chain(modelWorkday.labor).map(l => [l.staffMemberId, l]).fromPairs().value()
  182. const workday = allWorkdays[i]
  183. workday.labor.forEach(labor => {
  184. const modelLabor = modelLaborById[labor.staffMemberId]
  185. // modelLabor has hours, labor needs regularHours and overtimeHours
  186. labor.regularHours = Math.min(8, modelLabor.hours) || 0
  187. labor.overtimeHours = Math.max(0, modelLabor.hours - 8) || 0
  188. })
  189. // TODO: Update laborCost when wages are in
  190. workday.overtimeHours = workday.labor.map(l => l.overtimeHours || 0).reduce((a, b) => a + b, 0)
  191. workday.regularHours = workday.labor.map(l => l.regularHours || 0).reduce((a, b) => a + b, 0)
  192. workday.laborCost = workday.labor.map(l => {
  193. const staffMember = staffMembersById[l.staffMemberId]
  194. const wage = staffMember.wage || 0
  195. return ((l.regularHours || 0) * wage) + ((l.overtimeHours || 0) * wage * 1.5)
  196. }).reduce((a, b) => a + b, 0)
  197. if (workday.overtimeHours || workday.regularHours || !workday.isNewRecord) {
  198. await workday.save({ transaction })
  199. }
  200. }))
  201. await Promise.all(workday.labor.map(async labor => {
  202. if (labor.regularHours || labor.overtimeHours || !labor.isNewRecord) {
  203. await labor.save({ transaction })
  204. }
  205. }))
  206. await transaction.commit()
  207. res.status(200).end()
  208. } catch (err) {
  209. await transaction.rollback()
  210. throw err
  211. }
  212. }
  213. module.exports = {
  214. list,
  215. get,
  216. patch
  217. }