labor.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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 overtime = require('../overtime')
  7. const list = async (req, res) => {
  8. const terminalKey = req.params.terminal
  9. const terminal = await Terminal.findOne({where: {key: terminalKey}})
  10. if (!terminal) return res.status(404).end()
  11. const workdays = await Workday.findAll({where: { terminalId: terminal.id }})
  12. let workweeks = _.groupBy(workdays, d => formatDate(moment(d.date).startOf('week')))
  13. // fill workweeks
  14. getWeeks(10).forEach(week => {
  15. const w = formatDate(week)
  16. if (!workweeks[w]) workweeks[w] = []
  17. })
  18. // fill workdays
  19. workweeks = _.chain(workweeks)
  20. .toPairs()
  21. .map(([w, days]) => {
  22. days = _.chain(days)
  23. .map(d => [moment(d.date).weekday(), d])
  24. .fromPairs()
  25. .value()
  26. days = _.range(0, 7).map(d => days[d] || null)
  27. .map(day => day && {
  28. hours: day.hours,
  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. .map(sm => ({
  96. staffMemberId: sm.staffMemberId,
  97. hours: sm.hours
  98. }))
  99. // Restore any staffMembers that are no longer assigned this terminal
  100. labor.forEach(l => {
  101. if (!staffMembersById[l.staffMemberId]) {
  102. wd.labor.push(l)
  103. }
  104. })
  105. return wd
  106. })
  107. res.status(200).send({
  108. workdays: allWorkdays
  109. })
  110. }
  111. const patch = async (req, res) => {
  112. const transaction = await sequelize.transaction()
  113. try {
  114. const terminalKey = req.params.terminal
  115. const terminal = await Terminal.findOne({where: {key: terminalKey}})
  116. const week = parseDate(req.params.week)
  117. const workdays = await Workday.findAll({
  118. where: {
  119. terminalId: terminal.id,
  120. date: {
  121. [Op.gte]: week,
  122. [Op.lt]: moment(week).endOf('week')
  123. }
  124. },
  125. order: [ 'date' ]
  126. })
  127. const labor = await Labor.findAll({
  128. where: {
  129. workdayId: {
  130. [Op.in]: workdays.map(x => x.id)
  131. }
  132. }
  133. })
  134. const laborByWorkday = _.groupBy(labor, x => x.workdayId)
  135. const extraStaffMembers = _.chain(labor)
  136. .map(x => x.staffMemberId)
  137. .filter(x => x)
  138. .uniq()
  139. .value()
  140. const staffMembers = await StaffMember.findAll({
  141. where: {
  142. [Op.or]: [
  143. { terminalId: terminal.id },
  144. {
  145. id: {
  146. [Op.in]: extraStaffMembers
  147. }
  148. }
  149. ]
  150. },
  151. order: [ 'name' ]
  152. })
  153. const staffMembersById = _.chain(staffMembers).map(x => [x.id, x]).fromPairs().value()
  154. // Fill in empty days
  155. const workdaysByKey = _.chain(workdays).map(wd => [formatDate(wd.date), wd]).fromPairs().value()
  156. const allWorkdays = []
  157. for (let day = week, i = 0; i < 7; i++, day = day.add(1, 'day')) {
  158. const wd = workdaysByKey[formatDate(day)]
  159. allWorkdays[i] = wd || Workday.build({
  160. terminalId: terminal.id,
  161. date: week.add(i, 'day')
  162. })
  163. }
  164. const workdaysWithLabor = allWorkdays.map(wd => {
  165. const labor = laborByWorkday[wd.id] || []
  166. const laborByStaffMember = _.chain(labor).map(l => [l.staffMemberId, l]).fromPairs().value()
  167. // Map from staffMembers to preserve sorting
  168. wd.labor = staffMembers.map(sm => laborByStaffMember[sm.id] || Labor.build({staffMemberId: sm.id, workdayId: wd.id}))
  169. // Restore any staffMembers that are no longer assigned this terminal
  170. labor.forEach(l => {
  171. if (!staffMembersById[l.staffMemberId]) {
  172. wd.labor.push(l)
  173. }
  174. })
  175. return wd
  176. })
  177. // Update with model
  178. const model = req.body
  179. await Promise.all(model.workdays.map(async (modelWorkday, i) => {
  180. const modelLaborById = _.chain(modelWorkday.labor).map(l => [l.staffMemberId, l]).fromPairs().value()
  181. const workday = allWorkdays[i]
  182. workday.labor.forEach(labor => {
  183. const modelLabor = modelLaborById[labor.staffMemberId]
  184. const staffMember = staffMembersById[labor.staffMemberId]
  185. const priorHours = model.workdays
  186. .slice(0, i)
  187. .map(wd => wd.labor.find(l => l.staffMemberId === labor.staffMemberId).hours)
  188. .reduce((a, b) => a + b, 0)
  189. const hours = overtime(modelLabor.hours || 0, priorHours, terminalKey)
  190. labor.hours = modelLabor.hours || 0
  191. labor.regularHours = hours.regularHours
  192. labor.overtimeHours = hours.overtimeHours
  193. labor.doubletimeHours = hours.doubletimeHours
  194. labor.laborCost =
  195. (staffMember.wage * labor.regularHours || 0) +
  196. (staffMember.wage * labor.overtimeHours * 1.5 || 0) +
  197. (staffMember.wage * labor.doubletimeHours * 2.0 || 0) +
  198. (staffMember.salary / 2080 * labor.hours || 0)
  199. })
  200. workday.hours = workday.labor.map(l => l.hours || 0).reduce((a, b) => a + b, 0)
  201. workday.regularHours = workday.labor.map(l => l.regularHours || 0).reduce((a, b) => a + b, 0)
  202. workday.overtimeHours = workday.labor.map(l => l.overtimeHours || 0).reduce((a, b) => a + b, 0)
  203. workday.doubletimeHours = workday.labor.map(l => l.doubletimeHours || 0).reduce((a, b) => a + b, 0)
  204. workday.laborCost = workday.labor.map(l => l.laborCost || 0).reduce((a, b) => a + b, 0)
  205. if (workday.hours || !workday.isNewRecord) {
  206. await workday.save({ transaction })
  207. }
  208. await Promise.all(workday.labor.map(async labor => {
  209. if (labor.hours || !labor.isNewRecord) {
  210. await labor.save({ transaction })
  211. }
  212. }))
  213. }))
  214. await transaction.commit()
  215. res.status(200).end()
  216. } catch (err) {
  217. await transaction.rollback()
  218. throw err
  219. }
  220. }
  221. module.exports = {
  222. list,
  223. get,
  224. patch
  225. }