labor.js 8.6 KB

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