Explorar o código

Work week and labor charge

Alan Colon %!s(int64=8) %!d(string=hai) anos
pai
achega
23644a656e

+ 3 - 1
app/assets/index.js

@@ -8,5 +8,7 @@ module.exports = Object.assign(assets, {
   laborIcon: require('./noun_constructor_593655.svg'),
   crumbIcon: require('@alancnet/material-design-icons/image_ic_navigate_next_48px.svg'),
   clientIcon: require('@alancnet/material-design-icons/maps_ic_store_mall_directory_48px.svg'),
-  serviceIcon: require('./noun_service_406879.svg')
+  serviceIcon: require('./noun_service_406879.svg'),
+  calculatedIcon: require('./noun_calculate_605248.svg'),
+  dollarIcon: require('./noun_dollar_1083823.svg')
 })

+ 13 - 0
app/assets/noun_calculate_605248.svg

@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 100 125" xml:space="preserve">
+    <path d="M74,13H26c-2.2,0-4,1.8-4,4v66c0,2.2,1.8,4,4,4h48c2.2,0,4-1.8,4-4V17C78,14.8,76.2,13,74,13z M75,83c0,0.542-0.458,1-1,1  H26c-0.542,0-1-0.458-1-1V17c0-0.542,0.458-1,1-1h48c0.542,0,1,0.458,1,1V83z"/>
+    <path d="M40,71.5h-6c-0.828,0-1.5,0.671-1.5,1.5s0.672,1.5,1.5,1.5h6c0.828,0,1.5-0.671,1.5-1.5S40.828,71.5,40,71.5z"/>
+    <path d="M53,71.5h-6c-0.828,0-1.5,0.671-1.5,1.5s0.672,1.5,1.5,1.5h6c0.828,0,1.5-0.671,1.5-1.5S53.828,71.5,53,71.5z"/>
+    <path d="M66,71.5h-6c-0.828,0-1.5,0.671-1.5,1.5s0.672,1.5,1.5,1.5h6c0.828,0,1.5-0.671,1.5-1.5S66.828,71.5,66,71.5z"/>
+    <path d="M40,62.5h-6c-0.828,0-1.5,0.671-1.5,1.5c0,0.828,0.672,1.5,1.5,1.5h6c0.828,0,1.5-0.672,1.5-1.5  C41.5,63.171,40.828,62.5,40,62.5z"/>
+    <path d="M53,62.5h-6c-0.828,0-1.5,0.671-1.5,1.5c0,0.828,0.672,1.5,1.5,1.5h6c0.828,0,1.5-0.672,1.5-1.5  C54.5,63.171,53.828,62.5,53,62.5z"/>
+    <path d="M66,62.5h-6c-0.828,0-1.5,0.671-1.5,1.5c0,0.828,0.672,1.5,1.5,1.5h6c0.828,0,1.5-0.672,1.5-1.5  C67.5,63.171,66.828,62.5,66,62.5z"/>
+    <path d="M40,53.5h-6c-0.828,0-1.5,0.671-1.5,1.5c0,0.828,0.672,1.5,1.5,1.5h6c0.828,0,1.5-0.672,1.5-1.5  C41.5,54.171,40.828,53.5,40,53.5z"/>
+    <path d="M53,53.5h-6c-0.828,0-1.5,0.671-1.5,1.5c0,0.828,0.672,1.5,1.5,1.5h6c0.828,0,1.5-0.672,1.5-1.5  C54.5,54.171,53.828,53.5,53,53.5z"/>
+    <path d="M66,53.5h-6c-0.828,0-1.5,0.671-1.5,1.5c0,0.828,0.672,1.5,1.5,1.5h6c0.828,0,1.5-0.672,1.5-1.5  C67.5,54.171,66.828,53.5,66,53.5z"/>
+    <path d="M66,25H34c-1.1,0-2,0.9-2,2v16c0,1.1,0.9,2,2,2h32c1.1,0,2-0.9,2-2V27C68,25.9,67.1,25,66,25z M65,42H35V28h30V42z"/>
+</svg>

+ 3 - 0
app/assets/noun_dollar_1083823.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 100 125" xml:space="preserve">
+  <path d="M65.239,63.087c0-7.063-4.931-12.543-13.188-14.659c-0.017-0.004-0.034-0.008-0.051-0.013V25.695  c3.882,0.24,7.441,2.05,9.639,4.979c0.662,0.884,1.917,1.062,2.8,0.399c0.884-0.663,1.063-1.917,0.399-2.8  c-2.947-3.928-7.7-6.337-12.838-6.585v-5.752c0-1.104-0.896-2-2-2s-2,0.896-2,2v6.035c-7.546,1.43-13.239,7.545-13.239,14.862  c0,5.517,3.413,10.908,8.299,13.108c1.607,0.724,3.283,1.198,4.94,1.611v22.672c-3.882-0.24-7.441-2.05-9.639-4.979  c-0.663-0.884-1.916-1.062-2.8-0.399c-0.884,0.663-1.063,1.917-0.399,2.8c2.947,3.928,7.7,6.337,12.838,6.585v5.831  c0,1.104,0.896,2,2,2s2-0.896,2-2v-6.114C59.546,76.52,65.239,70.404,65.239,63.087z M44.703,46.296  c-3.499-1.576-5.942-5.467-5.942-9.461c0-5.131,3.92-9.454,9.239-10.768v21.351C46.847,47.107,45.738,46.762,44.703,46.296z   M52,73.855V52.573c3.37,1.04,9.238,3.81,9.239,10.514C61.239,68.218,57.319,72.542,52,73.855z"/>
+</svg>

+ 4 - 0
app/assets/noun_dollar_1833689.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" style="isolation:isolate" viewBox="0 0 32 40">
+  <path d=" M 11.175 18.341 L 13.122 18.035 Q 13.287 19.206 14.031 19.83 Q 14.786 20.453 16.131 20.453 Q 17.488 20.453 18.144 19.906 Q 18.801 19.348 18.801 18.604 Q 18.801 17.937 18.221 17.554 Q 17.816 17.291 16.208 16.886 Q 14.042 16.339 13.199 15.945 Q 12.368 15.54 11.93 14.84 Q 11.503 14.129 11.503 13.276 Q 11.503 12.499 11.853 11.842 Q 12.214 11.175 12.827 10.737 Q 13.287 10.398 14.074 10.168 Q 14.873 9.928 15.781 9.928 Q 17.149 9.928 18.177 10.322 Q 19.217 10.715 19.709 11.394 Q 20.201 12.061 20.387 13.188 L 18.462 13.451 Q 18.33 12.553 17.696 12.05 Q 17.072 11.547 15.923 11.547 Q 14.567 11.547 13.987 11.995 Q 13.407 12.444 13.407 13.046 Q 13.407 13.429 13.648 13.735 Q 13.888 14.053 14.403 14.26 Q 14.698 14.369 16.142 14.764 Q 18.232 15.322 19.053 15.683 Q 19.884 16.033 20.355 16.711 Q 20.825 17.389 20.825 18.396 Q 20.825 19.381 20.245 20.256 Q 19.676 21.12 18.593 21.602 Q 17.51 22.072 16.142 22.072 Q 13.877 22.072 12.685 21.132 Q 11.503 20.191 11.175 18.341 L 11.175 18.341 Z "/>
+  <path fill-rule="evenodd" d=" M 15.149 8.337 L 15.149 23.663 L 16.851 23.663 L 16.851 8.337 L 15.149 8.337 Z "/>
+</svg>

+ 58 - 21
app/components/labor-entry-page.js

@@ -1,6 +1,6 @@
 const app = require('../app')
 const moment = require('moment-immutable')
-const { editIcon } = require('../assets')
+const { editIcon, calculatedIcon, dollarIcon } = require('../assets')
 
 app.component('appLaborEntryPage', {
   template: html`
@@ -10,30 +10,63 @@ app.component('appLaborEntryPage', {
       { text: ctrl.terminalKey + ' Labor', link: '/labor/' + ctrl.terminalKey },
       { text: ctrl.startDate.format('L') + ' - ' + ctrl.endDate.format('L'), link: '/labor/' + ctrl.terminalKey + '/' + ctrl.startDate.format('YYYY-MM-DD') }
     ]"></app-breadcrumb>
-    <p>For week of {{::ctrl.startDate.format('LL')}} to {{::ctrl.endDate.format('LL')}}</p>
+    <p>For week of {{ctrl.startDate.format('LL')}} to {{ctrl.endDate.format('LL')}}</p>
+    <div>
+      <md-input-container class="md-margin">
+        <label>Labor Cost</label>
+        <input type="text" readonly value="{{ctrl.model.workweek.laborCost | currency}}" />
+        <md-icon md-svg-src="${calculatedIcon}">
+          <md-tooltip md-direction="down" style="height: 7em">
+            This field is calculated, <br/>
+            and cannot be edited directly. <br />
+            Save your changes to update.
+          </md-tooltip>
+        </md-icon>
+      </md-input-container>
+      <md-input-container class="md-margin">
+        <label>Labor Charge</label>
+        <input type="number" min="0" step="0.01" ng-model="ctrl.model.workweek.laborCharge" />
+        <md-icon md-svg-src="${dollarIcon}"></md-icon>
+      </md-input-container>
+      <md-input-container class="md-margin" ng-if="ctrl.model.workweek.laborCharge && ctrl.model.workweek.laborCost">
+        <label>Difference</label>
+        <input type="text" readonly value="{{(ctrl.model.workweek.laborCost - ctrl.model.workweek.laborCharge) | currency}} or {{(ctrl.model.workweek.laborCharge / ctrl.model.workweek.laborCost) * 100 | number : 1}}%" />
+        <md-icon md-svg-src="${calculatedIcon}">
+          <md-tooltip md-direction="down" style="height: 7em">
+            This field is calculated, <br/>
+            and cannot be edited directly. <br />
+            Save your changes to update.
+          </md-tooltip>
+        </md-icon>
+      </md-input-container>
+    </div>
     <form name="form" ng-submit="ctrl.submit()">
       <table md-table md-progress="ctrl.promise">
         <colgroup>
           <col style="width: 15%" />
-          <col ng-repeat="weekday in ::ctrl.weekdays" style="width: 12%" />
+          <col ng-repeat="weekday in ctrl.model.weekdays" style="width: 12%" />
         </colgroup>
         <thead md-head>
           <tr md-row>
             <th md-column>Staff Member</th>
-            <th md-column ng-repeat="weekday in ::ctrl.weekdays">
-              <span hide show-xs>{{::weekday.min}}</span>
-              <span hide show-sm>{{::weekday.short}}</span>
-              <span hide show-gt-sm>{{::weekday.name}}</span>
+            <th md-column ng-repeat="weekday in ctrl.weekdays">
+              <span hide show-xs>{{weekday.min}}</span>
+              <span hide show-sm>{{weekday.short}}</span>
+              <span hide show-gt-sm>{{weekday.name}}</span>
             </th>
           </tr>
         </thead>
         <tbody md-body>
-          <tr md-row ng-repeat="sfl in ::ctrl.staffMemberLabor track by sfl.id">
+          <tr md-row ng-repeat="sfl in ctrl.staffMemberLabor track by sfl.id">
             <td md-cell>
-              {{::ctrl.staffMembers[sfl.id].name}}
+              {{ctrl.staffMembers[sfl.id].name}}
             </td>
-            <td md-cell ng-repeat="day in ::sfl.days track by $index">
-              <input class="hour-input md-button md-raised" ng-model="day.hours" type="number" min="0" step="0.01">
+            <td md-cell ng-repeat="day in sfl.days track by $index">
+              <input
+                class="hour-input md-button md-raised"
+                tabindex="{{$index * ctrl.staffMemberLabor.length + ($parent.$index) + 1}}"
+                ng-model="day.hours"
+                type="number" min="0" step="0.01">
             </td>
           </tr>
         </tbody>
@@ -55,18 +88,21 @@ app.component('appLaborEntryPage', {
     api.staffMemberDictionary().then(staffMembers => {
       this.staffMembers = staffMembers
     })
-    this.promise = api.get(`/api/labor/${$routeParams.terminal}/${$routeParams.week}`).then(({workdays}) => {
-      this.model = workdays
-      const staffMemberIds = workdays[0].labor.map(x => x.staffMemberId)
-      this.staffMemberLabor = staffMemberIds.map((id, i) => ({
-        id,
-        days: this.model.map(wd => wd.labor[i])
-      }))
-    })
-
+    const load = () => {
+      this.promise = api.get(`/api/labor/${$routeParams.terminal}/${$routeParams.week}`).then((model) => {
+        this.model = model
+        const staffMemberIds = this.model.workdays[0].labor.map(x => x.staffMemberId)
+        this.staffMemberLabor = staffMemberIds.map((id, i) => ({
+          id,
+          days: this.model.workdays.map(wd => wd.labor[i])
+        }))
+      })
+    }
+    load()
     this.submit = async () => {
       const model = {
-        workdays: this.model.map(workday => ({
+        workweek: this.model.workweek,
+        workdays: this.model.workdays.map(workday => ({
           labor: workday.labor.map(labor => ({
             staffMemberId: labor.staffMemberId,
             hours: labor.hours || null
@@ -76,6 +112,7 @@ app.component('appLaborEntryPage', {
       try {
         await api.patch(`/api/labor/${$routeParams.terminal}/${$routeParams.week}`, model)
         $mdToast.showSimple('Labor saved.')
+        load()
       } catch (err) {
         window.err = err
         console.error(err)

+ 6 - 1
app/components/services-entry-page.js

@@ -33,7 +33,12 @@ app.component('appServicesEntryPage', {
               {{::ctrl.clients[sfl.id].name}}
             </td>
             <td md-cell ng-repeat="day in ::sfl.days track by $index">
-              <input class="hour-input md-button md-raised" ng-model="day.cartons" type="number" min="0" step="1">
+              <input
+                class="hour-input md-button md-raised"
+                ng-model="day.cartons"
+                tabindex="{{$index * ctrl.staffMemberLabor.length + ($parent.$index) + 1}}"
+                type="number" min="0" step="1">
+              
             </td>
           </tr>
         </tbody>

+ 1 - 0
auto-crud/index.js

@@ -2,6 +2,7 @@ require('./terminal')
 require('./labor-category')
 require('./service-category')
 require('./client')
+require('./workweek')
 /* Example */
 /*
 

+ 35 - 0
auto-crud/workweek.js

@@ -0,0 +1,35 @@
+const { register, Sequelize } = require('@alancnet/material-framework/auto-crud')
+
+register({
+  camelName: 'workweek',
+  // iconAsset: 'terminalIcon',
+  showNav: false,
+  schema: {
+    id: {
+      type: Sequelize.UUID,
+      defaultValue: Sequelize.UUIDV1,
+      primaryKey: true
+    },
+    startDate: Sequelize.DATEONLY,
+    terminalId: Sequelize.UUID,
+    hours: Sequelize.DOUBLE,
+    regularHours: Sequelize.DOUBLE,
+    overtimeHours: Sequelize.DOUBLE,
+    doubletimeHours: Sequelize.DOUBLE,
+    laborCost: Sequelize.DECIMAL(19, 4),
+    inbound: Sequelize.INTEGER,
+    delivered: Sequelize.INTEGER,
+    laborCharge: Sequelize.DECIMAL(19, 4)
+  },
+  options: {
+    paranoid: true,
+    indexes: [
+      {
+        unique: true,
+        fields: ['startDate', 'terminalId']
+      }
+    ]
+  },
+  columns: [],
+  layout: []
+})

+ 32 - 3
lib/controllers/labor.js

@@ -1,9 +1,10 @@
 const _ = require('lodash')
 const moment = require('moment-immutable')
 const { getWeeks, formatDate, parseDate } = require('../dates')
-const { StaffMember, Workday, Terminal, Labor, sequelize } = require('../database')
+const { StaffMember, Workday, Workweek, Terminal, Labor, sequelize } = require('../database')
 const { Op } = require('sequelize')
 const overtime = require('../overtime')
+const { dict } = require('@alancnet/material-framework/lib/util')
 
 const list = async (req, res) => {
   const terminalKey = req.params.terminal
@@ -19,6 +20,15 @@ const list = async (req, res) => {
     if (!workweeks[w]) workweeks[w] = []
   })
 
+  // Get workweek records
+  const weeks = dict(await Workweek.findAll({
+    where: {
+      startDate: {
+        [Op.in]: _.keys(workweeks)
+      }
+    }
+  }), ['startOfWeek'])
+
   // fill workdays
   workweeks = _.chain(workweeks)
     .toPairs()
@@ -33,8 +43,9 @@ const list = async (req, res) => {
           hours: day.hours,
           laborCost: day.laborCost
         })
-
+      const week = weeks[w]
       return {
+        ...week,
         workweek: w,
         workdays: days
       }
@@ -60,6 +71,9 @@ const get = async (req, res) => {
     },
     order: [ 'date' ]
   })
+
+  const workweek = (await Workweek.findOne({where: {startDate: week, terminalId: terminal.id}})) || {startDate: week}
+
   const labor = await Labor.findAll({
     where: {
       workdayId: {
@@ -116,6 +130,7 @@ const get = async (req, res) => {
   })
 
   res.status(200).send({
+    workweek,
     workdays: allWorkdays
   })
 }
@@ -136,6 +151,8 @@ const patch = async (req, res) => {
       },
       order: [ 'date' ]
     })
+    const workweek = (await Workweek.findOne({where: {startDate: week, terminalId: terminal.id}})) || Workweek.build({startDate: week, terminalId: terminal.id})
+
     const labor = await Labor.findAll({
       where: {
         workdayId: {
@@ -191,6 +208,11 @@ const patch = async (req, res) => {
 
     // Update with model
     const model = req.body
+    workweek.hours = 0
+    workweek.regularHours = 0
+    workweek.overtimeHours = 0
+    workweek.doubletimeHours = 0
+    workweek.laborCost = 0
     await Promise.all(model.workdays.map(async (modelWorkday, i) => {
       const modelLaborById = _.chain(modelWorkday.labor).map(l => [l.staffMemberId, l]).fromPairs().value()
       const workday = allWorkdays[i]
@@ -218,6 +240,12 @@ const patch = async (req, res) => {
       workday.doubletimeHours = workday.labor.map(l => l.doubletimeHours || 0).reduce((a, b) => a + b, 0)
       workday.laborCost = workday.labor.map(l => l.laborCost || 0).reduce((a, b) => a + b, 0)
 
+      workweek.hours += workday.hours
+      workweek.regularHours += workday.regularHours
+      workweek.overtimeHours += workday.overtimeHours
+      workweek.doubletimeHours += workday.doubletimeHours
+      workweek.laborCost += workday.laborCost
+
       if (workday.hours || !workday.isNewRecord) {
         await workday.save({ transaction })
       }
@@ -229,7 +257,8 @@ const patch = async (req, res) => {
       }))
 
     }))
-
+    workweek.laborCharge = model.workweek.laborCharge
+    await workweek.save({ transaction })
 
     await transaction.commit()
     

+ 9 - 0
lib/database/migrations/1.06-workweek.js

@@ -0,0 +1,9 @@
+module.exports = {
+  version: 1.06,
+  name: 'Workweek',
+  description: 'Adds the workweek table',
+  up: async (queryInterface, Sequelize) => {
+    await queryInterface.sequelize.models.workweek.sync()
+    return 1.06
+  }
+}

+ 2 - 1
lib/database/migrations/index.js

@@ -5,7 +5,8 @@ module.exports = Object.assign(migrations, {
     require('./1.02-retailer-to-client.js'),
     require('./1.03-service-categories.js'),
     require('./1.04-labor-hours.js'),
-    require('./1.05-inbound-delivered.js')
+    require('./1.05-inbound-delivered.js'),
+    require('./1.06-workweek.js')
   ]
 })
 

+ 2 - 0
lib/database/seeds/dev.js

@@ -84,6 +84,7 @@ const users = async (db) => {
   )
 
   const laxLabor = {
+    workweek: {},
     workdays: [
       {
         labor: [
@@ -145,6 +146,7 @@ const users = async (db) => {
   }
 
   const lasLabor = {
+    workweek: {},
     workdays: [
       {
         labor: [

+ 1 - 1
lib/overtime.js

@@ -21,7 +21,7 @@ const californiaRules = (hours) => {
 }
 
 const federalRules = (hours, priorHours) => {
-  const regularHours = Math.min(40 - priorHours, hours)
+  const regularHours = Math.max(Math.min(40 - priorHours, hours), 0)
   const overtimeHours = hours - regularHours
   return {regularHours, overtimeHours, doubletimeHours: 0}
 }