Alan Colon 7 лет назад
Родитель
Сommit
b9ac199ee6

+ 95 - 52
app/components/dashboard-page.js

@@ -3,60 +3,103 @@ const app = require('../app')
 app.component('appDashboardPage', {
   template: html`
     <app-user-area title-text="Dashboard">
-      <md-grid-list
-        md-cols="1" md-cols-sm="2" md-cols-md="3" md-cols-gt-md="6"
-        md-row-height-gt-md="1:1" md-row-height="4:3"
-        md-gutter="8px" md-gutter-gt-sm="4px">
-
-        <md-grid-tile
-          md-rowspan="2"
-          md-colspan="3"
-          md-colspan-sm="2"
-          md-rowspan-sm="1"
-          md-colspan-xs="1"
-          md-colspan-xs="1"
-          ng-class="tile.background">
-          <canvas id="line" class="chart chart-line" chart-data="dashboard.efficiency.data"
-            chart-labels="dashboard.efficiency.labels" chart-series="dashboard.efficiency.series" chart-options="dashboard.efficiency.options">
-          </canvas>
-          <md-grid-tile-footer><h3>Efficiency</h3></md-grid-tile-footer>
-        </md-grid-tile>
-
-        <md-grid-tile
-          md-rowspan="2"
-          md-colspan="3"
-          md-colspan-sm="2"
-          md-rowspan-sm="1"
-          md-colspan-xs="1"
-          md-colspan-xs="1"
-          ng-class="tile.background">
-          <canvas id="line" class="chart chart-line" chart-data="dashboard.delivery.data"
-            chart-labels="dashboard.delivery.labels" chart-series="dashboard.delivery.series" chart-options="dashboard.delivery.options">
-          </canvas>
-          <md-grid-tile-footer><h3>Delivery</h3></md-grid-tile-footer>
-        </md-grid-tile>
-
-        <md-grid-tile
-          md-rowspan="2"
-          md-colspan="3"
-          md-colspan-sm="2"
-          md-rowspan-sm="1"
-          md-colspan-xs="1"
-          md-colspan-xs="1"
-          ng-class="tile.background">
-          <canvas id="line" class="chart chart-line" chart-data="dashboard.laborCost.data"
-            chart-labels="dashboard.laborCost.labels" chart-series="dashboard.laborCost.series" chart-options="dashboard.laborCost.options">
-          </canvas>
-          <md-grid-tile-footer><h3>Labor Cost</h3></md-grid-tile-footer>
-        </md-grid-tile>
-
-      </md-grid-list>
+      <div flex layout="row">
+        <md-card flex ng-repeat="serviceCategory in $ctrl.statistics.serviceCategories">
+          <md-card-content>
+            <h2>{{::$ctrl.serviceCategories[serviceCategory.serviceCategory].name}}</h2>
+            <div ng-repeat="terminal in serviceCategory.terminals">
+              <h3>{{::$ctrl.terminals[terminal.terminal].name}}</h3>
+              <table md-table>
+                <tbody>
+                  <tr ng-repeat="laborCategory in terminal.laborCategories">
+                    <th align="left">{{::$ctrl.laborCategories[laborCategory.laborCategory].name}}</th>
+                    <td align="right">{{laborCategory.costPerCarton | currency}}</td>
+                  </tr>
+                  <tr ng-if="terminal.laborCategories.length > 1">
+                    <th align="left">All</th>
+                    <td align="right">{{terminal.costPerCarton | currency}}</td>
+                  </tr>
+                </tbody>
+              </table>
+              <hr />
+            </div>
+            <div ng-if="serviceCategory.terminals.length > 1">
+              <h3>Overall</h3>
+              <table md-table>
+                <tbody>
+                  <tr ng-repeat="laborCategory in serviceCategory.laborCategories">
+                    <th align="left">{{::$ctrl.laborCategories[laborCategory.laborCategory].name}}</th>
+                    <td align="right">{{laborCategory.costPerCarton | currency}}</td>
+                  </tr>
+                  <tr ng-if="serviceCategory.laborCategories.length > 1">
+                    <th align="left">All</th>
+                    <td align="right">{{serviceCategory.costPerCarton | currency}}</td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+          </md-card-content>
+        </md-card>
+      </div>
+      <div flex layout="column" layout-gt-sm="row">
+        <md-card flex="50" class="mg-margin md-padding">
+          <md-card-title>
+            <md-card-title-text>
+              <span class="md-headline">Labor Costs</span>
+              <span class="md-subhead"></span>
+            </md-card-title-text>
+          </md-card-title>
+          <md-card-content>
+            <div style="width: 520px; height: 260px;" >
+              <canvas class="chart chart-line" chart-data="$ctrl.charts.laborCost.data" chart-colors="$ctrl.charts.laborCost.colors"
+                chart-labels="$ctrl.charts.laborCost.labels" chart-series="$ctrl.charts.laborCost.series" chart-options="$ctrl.charts.laborCost.options">
+              </canvas>
+            </div>
+            <div ng-repeat="color in $ctrl.charts.laborCost.colors">
+              <div style="background-color: {{color}}; width: 16px; height: 16px; display: inline-block; margin: 0 4px;"></div>
+              {{$ctrl.charts.laborCost.series[$index]}}
+            </div>
+          </md-card-content>
+        </md-card>
+        <md-card flex="50" class="mg-margin md-padding">
+          <md-card-title>
+            <md-card-title-text>
+              <span class="md-headline">Efficiency</span>
+              <span class="md-subhead"></span>
+            </md-card-title-text>
+          </md-card-title>
+          <md-card-content>
+            <div style="width: 520px; height: 260px;" >
+              <canvas class="chart chart-line" chart-data="$ctrl.charts.efficiency.data" chart-colors="$ctrl.charts.efficiency.colors"
+                chart-labels="$ctrl.charts.efficiency.labels" chart-series="$ctrl.charts.efficiency.series" chart-options="$ctrl.charts.efficiency.options">
+              </canvas>
+            </div>
+            <div ng-repeat="color in $ctrl.charts.efficiency.colors">
+              <div style="background-color: {{color}}; width: 16px; height: 16px; display: inline-block; margin: 0 4px;"></div>
+              {{$ctrl.charts.efficiency.series[$index]}}
+            </div>
+          </md-card-content>
+        </md-card>
+      </div>
     </app-user-area>
   `,
-  controllerAs: 'dashboard',
-  controller: function(statistics) {
-    statistics.efficiency().then(statistics => {
-      Object.assign(this, statistics)
+  controller: function(api, statistics) {
+    api.statistics().then(stats => {
+      this.statistics = stats
+      this.charts = statistics.charts(stats.metricsOverTime)
     })
+    api.terminals().then(terminals => {
+      this.terminals = terminals
+    })
+    api.laborCategories().then(laborCategories => {
+      this.laborCategories = laborCategories
+    })
+    api.serviceCategories().then(serviceCategories => {
+      this.serviceCategories = serviceCategories
+    })
+
+  //   statistics.efficiency().then(statistics => {
+  //     Object.assign(this, statistics)
+  //   })
   }
 })

+ 1 - 1
app/components/labor-page.js

@@ -28,7 +28,7 @@ app.component('appLaborPage', {
             <td md-cell ng-repeat="workday in ::week.workdays track by $index">
               <div>
                 <span ng-if="::workday" style="white-space: nowrap;">
-                  {{::workday.regularHours + workday.overtimeHours}}
+                  {{::workday.hours}}
                   <span hide show-xs>h</span>
                   <span hide show-sm>hrs</span>
                   <span hide show-gt-sm>hours</span>

+ 16 - 7
app/services/api.js

@@ -1,21 +1,22 @@
 const app = require('../app')
 const _ = require('lodash')
+const { dict } = require('@alancnet/material-framework/lib/util')
 
 app.run(function(api) {
   api.statistics = () => api.get('/api/statistics')
 
   let terminals = null
-  api.terminals = () => terminals || (terminals = api.get('/api/terminals'))
+  api.terminals = () => terminals || (terminals = api.get('/api/terminals').then(dict))
 
-  let terminalsDictionary = null
-  api.terminalsDictionary = () => terminalsDictionary || (terminalsDictionary = api.terminals().then(terminals => 
+  let terminalDictionary = null
+  api.terminalDictionary = () => terminalDictionary || (terminalDictionary = api.terminals().then(terminals => 
     _.fromPairs(terminals.map(loc => [loc.key, loc]))
   ))
   api.terminal = (key) => api.terminals().then(async terminals =>
-    (await api.terminalsDictionary())[key]
+    (await api.terminalDictionary())[key]
   )
   let staffMembers = null
-  api.staffMembers = () => staffMembers || (staffMembers = api.get('/api/staff-members/all'))
+  api.staffMembers = () => staffMembers || (staffMembers = api.get('/api/staff-members/all').then(dict))
 
   let staffMemberDictionary = null
   api.staffMemberDictionary = () => staffMemberDictionary || (staffMemberDictionary = api.staffMembers().then(staffMembers =>
@@ -23,7 +24,7 @@ app.run(function(api) {
   ))
 
   let clients = null
-  api.clients = () => clients || (clients = api.get('/api/clients/all'))
+  api.clients = () => clients || (clients = api.get('/api/clients/all').then(dict))
 
   let clientDictionary = null
   api.clientDictionary = () => clientDictionary || (clientDictionary = api.clients().then(clients =>
@@ -31,11 +32,19 @@ app.run(function(api) {
   ))
 
   let laborCategories = null
-  api.laborCategories = () => laborCategories || (laborCategories = api.get('/api/labor-categories'))
+  api.laborCategories = () => laborCategories || (laborCategories = api.get('/api/labor-categories').then(dict))
 
   let laborCategoryDictionary= null
   api.laborCategoryDictionary = () => laborCategoryDictionary || (laborCategoryDictionary = api.laborCategories().then(laborCategories =>
     _.fromPairs(laborCategories.map(lc => [lc.id, lc]))
   ))
 
+  let serviceCategories = null
+  api.serviceCategories = () => serviceCategories || (serviceCategories = api.get('/api/service-categories').then(dict))
+
+  let serviceCategoryDictionary= null
+  api.serviceCategoryDictionary = () => serviceCategoryDictionary || (serviceCategoryDictionary = api.serviceCategories().then(serviceCategories =>
+    _.fromPairs(serviceCategories.map(lc => [lc.id, lc]))
+  ))
+
 })

+ 43 - 11
app/services/statistics.js

@@ -1,7 +1,34 @@
 const _ = require('lodash')
 const app = require('../app')
 
-app.service('statistics', function(api) {
+function trim (str) {
+  return str.replace(/^\s+|\s+$/gm,'');
+}
+
+function rgbaToHex (rgba) {
+  const parts = rgba.substring(rgba.indexOf("(")).split(","),
+    r = parseInt(trim(parts[0].substring(1)), 10),
+    g = parseInt(trim(parts[1]), 10),
+    b = parseInt(trim(parts[2]), 10),
+    a = parseFloat(trim(parts[3].substring(0, parts[3].length - 1))).toFixed(2)
+    return ('#' + r.toString(16).padStart(2, 0) + g.toString(16).padStart(2, 0) + b.toString(16).padStart(2, 0))
+}
+
+app.service('statistics', function(api, $mdColors) {
+  window.$mdColors = $mdColors
+  const colors = [
+
+    rgbaToHex($mdColors.getThemeColor('blue')),
+    rgbaToHex($mdColors.getThemeColor('red')),
+    rgbaToHex($mdColors.getThemeColor('amber')),
+    rgbaToHex($mdColors.getThemeColor('teal')),
+    rgbaToHex($mdColors.getThemeColor('deep-orange')),
+    rgbaToHex($mdColors.getThemeColor('cyan')),
+    rgbaToHex($mdColors.getThemeColor('purple')),
+    rgbaToHex($mdColors.getThemeColor('lime')),
+    rgbaToHex($mdColors.getThemeColor('indigo')),
+    rgbaToHex($mdColors.getThemeColor('pink'))
+  ]
   const chart = ({
     rows,
     seriesField,
@@ -28,22 +55,27 @@ app.service('statistics', function(api) {
       return labels.map(l => sdata[l])
     })
 
-    return {labels, series, data, options: {
-      tooltips: {
-        callbacks: {
-          label: (tooltipItem, data) => {
-            const label = data.datasets[tooltipItem.datasetIndex].label || ''
-            const value = tooltipItem.yLabel
-            return format ? format(label, value) : `${label}: ${value}`
+    return {
+      labels,
+      series,
+      data,
+      colors: colors.slice(0, series.length),
+      options: {
+        tooltips: {
+          callbacks: {
+            label: (tooltipItem, data) => {
+              const label = data.datasets[tooltipItem.datasetIndex].label || ''
+              const value = tooltipItem.yLabel
+              return format ? format(label, value) : `${label}: ${value}`
+            }
           }
         }
       }
-    }}
+    }
   }
 
 
-  this.efficiency = async () => {
-    const rows = await api.statistics()
+  this.charts = (rows) => {
     return {
       delivery: chart({
         rows,

+ 1 - 0
auto-crud/service-category.js

@@ -14,6 +14,7 @@ register({
       type: Sequelize.STRING,
       unique: true
     },
+    displayOrder: Sequelize.INTEGER
   },
   options: {
     paranoid: true,

+ 5 - 1
bin/project.js

@@ -3,4 +3,8 @@ const frameworkConfig = require('@alancnet/material-framework/config')
 frameworkConfig.inject(config)
 require('../lib/server')
 require('../lib/database')
-require('@alancnet/material-framework/bin/project')
+const { vorpal } = require('@alancnet/material-framework/bin/project')
+
+vorpal.command('labor', 'Processes overtime, labor costs, etc.')
+.action(require('../jobs/labor'))
+

+ 16 - 0
jobs/labor.js

@@ -0,0 +1,16 @@
+const _ = require('lodash')
+const database = require('../lib/database')
+const { StaffMember, Workday, Terminal, LaborCategory, ServiceCategory, Labor } = database
+const moment = require('moment-immutable')
+const { dict } = require('@alancnet/material-framework/lib/util')
+
+
+module.exports = async () => {
+  const staffMembers = dict(await StaffMember.findAll())
+  const workdays = dict(await Workday.findAll())
+  const terminals = dict(await Terminal.findAll())
+  const laborCategories = dict(await LaborCategory.findAll())
+  const serviceCategories = dict(await ServiceCategory.findAll())
+  const labor = dict(await Labor.findAll())
+
+}

+ 31 - 22
lib/controllers/labor.js

@@ -3,6 +3,7 @@ const moment = require('moment-immutable')
 const { getWeeks, formatDate, parseDate } = require('../dates')
 const { StaffMember, Workday, Terminal, Labor, sequelize } = require('../database')
 const { Op } = require('sequelize')
+const overtime = require('../overtime')
 
 const list = async (req, res) => {
   const terminalKey = req.params.terminal
@@ -11,7 +12,7 @@ const list = async (req, res) => {
   const workdays = await Workday.findAll({where: { terminalId: terminal.id }})
 
   let workweeks = _.groupBy(workdays, d => formatDate(moment(d.date).startOf('week')))
-  
+
   // fill workweeks
   getWeeks(10).forEach(week => {
     const w = formatDate(week)
@@ -29,8 +30,7 @@ const list = async (req, res) => {
       
       days = _.range(0, 7).map(d => days[d] || null)
         .map(day => day && {
-          regularHours: day.regularHours,
-          overtimeHours: day.overtimeHours,
+          hours: day.hours,
           laborCost: day.laborCost
         })
 
@@ -102,10 +102,9 @@ const get = async (req, res) => {
     // Map from staffMembers to preserve sorting
     wd.labor = staffMembers
       .map(sm => laborByStaffMember[sm.id] || {staffMemberId: sm.id})
-      // Labor requires regularHours and overtimeHours, model requires hours
       .map(sm => ({
         staffMemberId: sm.staffMemberId,
-        hours: (sm.regularHours + sm.overtimeHours) || null
+        hours: sm.hours
       }))
     // Restore any staffMembers that are no longer assigned this terminal
     labor.forEach(l => {
@@ -197,31 +196,41 @@ const patch = async (req, res) => {
       const workday = allWorkdays[i]
       workday.labor.forEach(labor => {
         const modelLabor = modelLaborById[labor.staffMemberId]
-        // modelLabor has hours, labor needs regularHours and overtimeHours
-        labor.regularHours = Math.min(8, modelLabor.hours) || 0
-        labor.overtimeHours = Math.max(0, modelLabor.hours - 8) || 0
+        const staffMember = staffMembersById[labor.staffMemberId]
+        const priorHours = model.workdays
+          .slice(0, i)
+          .map(wd => wd.labor.find(l => l.staffMemberId === labor.staffMemberId).hours)
+          .reduce((a, b) => a + b, 0)
+        const hours = overtime(modelLabor.hours || 0, priorHours, terminalKey)
+        labor.hours = modelLabor.hours || 0
+        labor.regularHours = hours.regularHours
+        labor.overtimeHours = hours.overtimeHours
+        labor.doubletimeHours = hours.doubletimeHours
+        labor.laborCost = 
+          (staffMember.wage * labor.regularHours || 0) +
+          (staffMember.wage * labor.overtimeHours * 1.5 || 0) +
+          (staffMember.wage * labor.doubletimeHours * 2.0 || 0) +
+          (staffMember.salary / 2080 * labor.hours || 0)
       })
-      // TODO: Update laborCost when wages are in
-      workday.overtimeHours = workday.labor.map(l => l.overtimeHours || 0).reduce((a, b) => a + b, 0)
+      workday.hours = workday.labor.map(l => l.hours || 0).reduce((a, b) => a + b, 0)
       workday.regularHours = workday.labor.map(l => l.regularHours || 0).reduce((a, b) => a + b, 0)
-      workday.laborCost = workday.labor.map(l => {
-        const staffMember = staffMembersById[l.staffMemberId]
-        const wage = staffMember.wage || 0
-        return ((l.regularHours || 0) * wage) + ((l.overtimeHours || 0) * wage * 1.5)
-      }).reduce((a, b) => a + b, 0)
-      
-      if (workday.overtimeHours || workday.regularHours || !workday.isNewRecord) {
+      workday.overtimeHours = workday.labor.map(l => l.overtimeHours || 0).reduce((a, b) => a + b, 0)
+      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)
+
+      if (workday.hours || !workday.isNewRecord) {
         await workday.save({ transaction })
       }
 
-    }))
+      await Promise.all(workday.labor.map(async labor => {
+        if (labor.hours || !labor.isNewRecord) {
+          await labor.save({ transaction })
+        }
+      }))
 
-    await Promise.all(workday.labor.map(async labor => {
-      if (labor.regularHours || labor.overtimeHours || !labor.isNewRecord) {
-        await labor.save({ transaction })
-      }
     }))
 
+
     await transaction.commit()
     
     res.status(200).end()

+ 85 - 4
lib/controllers/statistics.js

@@ -1,19 +1,20 @@
+const _ = require('lodash')
 const { Terminal } = require('../database')
 const sequelize = require('../database/sequelize')
+const { sanitize } = require('@alancnet/material-framework/lib/util')
 
 const get = async (req, res) => {
   const terminalIds = (await Terminal.findAll())
     .filter(loc => req.claims.TERMINAL_ALL_ACCESS || req.claims[`TERMINAL_${loc.key}_ACCESS`])
     .map(loc => loc.id)
 
-  const [results, metadata] = await sequelize.query(`
+  const [metricsOverTime, metadata] = await sequelize.query(`
     SELECT
       loc.key,
       wd.date,
       wd.laborCost,
-      SUM(svc.delivered) as delivered,
-      SUM(svc.scanned) as scanned,
-      SUM(svc.delivered) / (wd.regularHours + (wd.overtimeHours * 1.5)) as efficiency
+      SUM(distinct svc.cartons) as cartons,
+      cast(SUM(distinct wd.laborCost) as double) / SUM(distinct svc.cartons) as efficiency
     FROM workdays wd
     JOIN terminals loc on wd.terminalId = loc.id
     LEFT JOIN services svc on svc.workdayId = wd.id
@@ -22,6 +23,86 @@ const get = async (req, res) => {
   `, {
     replacements: { terminalIds }
   })
+
+  const metricsSql = `
+    select
+        terminals.key as terminal,
+        serviceCategories.key as serviceCategory,
+        serviceCategories.displayOrder,
+        laborCategories.key as laborCategory,
+        sum(distinct labors.laborCost) as laborCost,
+        sum(distinct services.cartons) as cartons,
+        cast(sum(distinct labors.laborCost) as double) / sum(distinct services.cartons) as costPerCarton
+
+    -- Start with Terminals
+    from terminals
+
+    -- Link to labor cost
+    join workdays on workdays.terminalId = terminals.id
+    join services on services.workdayId = workdays.id
+    join labors on labors.workdayId = workdays.id
+
+    -- Link to LaborCategory
+    join staffMembers on labors.staffMemberId = staffMembers.id
+    join laborCategories on staffMembers.laborCategoryId = laborCategories.id
+
+    -- Link to ServiceCategory
+    join laborServiceCategories on laborServiceCategories.laborCategoryId = laborCategories.id
+    join serviceCategories on laborServiceCategories.serviceCategoryId = serviceCategories.id
+    group by terminals.key, serviceCategories.key, laborCategories.key
+  `
+
+  const metricsPerServiceCategorySql = `
+    select
+      serviceCategory,
+      displayOrder,
+      cast(sum(distinct laborCost) as double) / sum(distinct cartons) as costPerCarton
+    from (${metricsSql})
+    group by serviceCategory
+    order by displayOrder
+  `
+
+  const metricsPerLaborCategorySql = `
+    select
+      serviceCategory,
+      laborCategory,
+      cast(sum(distinct laborCost) as double) / sum(distinct cartons) as costPerCarton
+    from (${metricsSql})
+    group by laborCategory
+  `
+
+  const metricsPerTerminalAndServiceCategorySql = `
+    select
+      terminal,
+      serviceCategory,
+      cast(sum(distinct laborCost) as double) / sum(distinct cartons) as costPerCarton
+    from (${metricsSql})
+    group by terminal, serviceCategory
+  `
+
+  const metricsPerTerminalAndLaborCategorySql = `
+    select
+      terminal,
+      serviceCategory,
+      laborCategory,
+      cast(sum(distinct laborCost) as double) / sum(distinct cartons) as costPerCarton
+    from (${metricsSql})
+    group by terminal, laborCategory
+  `
+  const metricsPerServiceCategory = await sanitize(req, (await sequelize.query(metricsPerServiceCategorySql))[0])
+  const metricsPerLaborCategory = await sanitize(req, (await sequelize.query(metricsPerLaborCategorySql))[0])
+  const metricsPerTerminalAndServiceCategory = await sanitize(req, (await sequelize.query(metricsPerTerminalAndServiceCategorySql))[0])
+  const metricsPerTerminalAndLaborCategory = await sanitize(req, (await sequelize.query(metricsPerTerminalAndLaborCategorySql))[0])
+
+  const results = {
+    serviceCategories: metricsPerServiceCategory.map(mpsc => Object.assign(mpsc, {
+      laborCategories: metricsPerLaborCategory.filter(mplc => mplc.serviceCategory === mpsc.serviceCategory),
+      terminals: metricsPerTerminalAndServiceCategory.filter(mpt => mpt.serviceCategory === mpsc.serviceCategory).map(mpt => Object.assign(mpt, {
+        laborCategories: metricsPerTerminalAndLaborCategory.filter(mptlc => mptlc.serviceCategory === mpsc.serviceCategory && mptlc.terminal === mpt.terminal)
+      }))
+    })),
+    metricsOverTime
+  }
   res.status(200).send(results)
 
 }

+ 16 - 1
lib/database/index.js

@@ -15,6 +15,12 @@ const StaffMember = require('./staff-member')
 const StaffingAgency = require('./staffing-agency')
 const Labor = require('./labor')
 
+const StaffMemberLabor = StaffMember.hasMany(Labor)
+const LaborStaffMember = Labor.belongsTo(StaffMember)
+
+const WorkdayLabor = Workday.hasMany(Labor)
+const LaborWorkday = Labor.belongsTo(Workday)
+
 const TerminalWorkday = Terminal.hasMany(Workday)
 const WorkdayTerminal = Workday.belongsTo(Terminal)
 
@@ -27,6 +33,9 @@ const ServiceClient = Service.belongsTo(Client)
 const TerminalStaffMember = Terminal.hasMany(StaffMember)
 const StaffMemberTerminal = StaffMember.belongsTo(Terminal)
 
+const LaborCategoryStaffMember = LaborCategory.hasMany(StaffMember)
+const StaffMemberLaborCategory = StaffMember.belongsTo(LaborCategory)
+
 const TerminalClient = Terminal.hasMany(Client)
 const ClientTerminal = Client.belongsTo(Terminal)
 
@@ -60,5 +69,11 @@ module.exports = Object.assign(database, {
   TerminalClient,
   ClientTerminal,
   LaborServiceCategory,
-  ServiceLaborCategory
+  ServiceLaborCategory,
+  LaborCategoryStaffMember,
+  StaffMemberLaborCategory,
+  StaffMemberLabor,
+  LaborStaffMember,
+  WorkdayLabor,
+  LaborWorkday
 })

+ 8 - 218
lib/database/initialize.js

@@ -1,225 +1,15 @@
-const { Op } = require('sequelize')
-const { controllers: C } = require('@alancnet/material-framework/server')
-const { register } = C.auth.permissions
-const terminals = [
-  {
-    name: 'Las Vegas',
-    key: 'LAS'
-  },
-  {
-    name: 'Los Angeles',
-    key: 'LAX'
-  },
-  {
-    name: 'Phoenix',
-    key: 'PHX'
-  },
-  {
-    name: 'San Francisco',
-    key: 'SFO'
-  }
-]
-
-const laborCategories = [
-  {
-    name: 'Warehouse',
-    key: 'WAREHOUSE'
-  },
-  {
-    name: 'Admin',
-    key: 'ADMIN'
-  },
-  {
-    name: 'Delivery',
-    key: 'DELIVERY'
-  },
-  {
-    name: 'Ops',
-    key: 'OPS'
-  }
-]
-
-const serviceCategories = [
-  {
-    name: 'Inbound',
-    key: 'INBOUND',
-    laborCategories: [
-      'WAREHOUSE',
-      'ADMIN',
-      'OPS'
-    ]
-  },
-  {
-    name: 'Delivery',
-    key: 'DELIVERY',
-    laborCategories: [
-      'DELIVERY'
-    ]
-  }
-]
-
-const roles = [
-  {
-    name: 'Management',
-    key: 'MANAGER',
-    permissions: [
-      'LABOR_CATEGORY_CREATE',
-      'LABOR_CATEGORY_DELETE',
-      'LABOR_CATEGORY_READ',
-      'LABOR_CATEGORY_UNDELETE',
-      'LABOR_CATEGORY_UPDATE',
-      'LABOR_ENTRY',
-      'LABOR_VIEW',
-      'TERMINAL_ALL_ACCESS',
-      'TERMINAL_CREATE',
-      'TERMINAL_DELETE',
-      'TERMINAL_READ',
-      'TERMINAL_UNDELETE',
-      'TERMINAL_UPDATE',
-      'METRICS_VIEW',
-      'CLIENT_CREATE',
-      'CLIENT_DELETE',
-      'CLIENT_READ',
-      'CLIENT_UNDELETE',
-      'CLIENT_UPDATE',
-      'ROLE_CREATE',
-      'ROLE_DELETE',
-      'ROLE_READ',
-      'ROLE_UNDELETE',
-      'ROLE_UPDATE',
-      'SERVICES_ENTRY',
-      'SERVICES_VIEW',
-      'STAFFING_AGENCY_CREATE',
-      'STAFFING_AGENCY_DELETE',
-      'STAFFING_AGENCY_READ',
-      'STAFFING_AGENCY_UNDELETE',
-      'STAFFING_AGENCY_UPDATE',
-      'STAFF_MEMBER_CREATE',
-      'STAFF_MEMBER_DELETE',
-      'STAFF_MEMBER_READ',
-      'STAFF_MEMBER_UNDELETE',
-      'STAFF_MEMBER_UPDATE',
-      'USER_CREATE',
-      'USER_DELETE',
-      'USER_READ',
-      'USER_UNDELETE',
-      'USER_UPDATE',
-      'INCOME_ADMIN_VIEW',
-      'INCOME_DELIVERY_VIEW',
-      'INCOME_OPS_VIEW',
-      'INCOME_WAREHOUSE_VIEW',
-      'VIEW_TERMINAL_LAS',
-      'VIEW_TERMINAL_LAX',
-      'VIEW_TERMINAL_PHX',
-      'VIEW_TERMINAL_SFO'
-    ].join(',')
-  },
-  {
-    name: 'Terminal Manager',
-    key: 'TERMINAL_MANAGER',
-    permissions: [
-      'LABOR_CATEGORY_READ',
-      'TERMINAL_READ',
-      'METRICS_VIEW',
-      'CLIENT_CREATE',
-      'CLIENT_DELETE',
-      'CLIENT_READ',
-      'CLIENT_UNDELETE',
-      'CLIENT_UPDATE',
-      'CLIENT_VIEW',
-      'STAFFING_AGENCY_READ',
-      'STAFF_MEMBER_CREATE',
-      'STAFF_MEMBER_DELETE',
-      'STAFF_MEMBER_READ',
-      'STAFF_MEMBER_UNDELETE',
-      'STAFF_MEMBER_UPDATE',
-      'STAFF_MEMBER_VIEW'
-    ]
-      .concat(laborCategories.map(x => x.key).filter(key => key !== 'ADMIN').map(key => `INCOME_${key}_VIEW`))
-      .join(',')
-  },
-  {
-    name: 'Accounting',
-    key: 'ACCOUNTING',
-    permissions: [
-      'LABOR_CATEGORY_READ',
-      'TERMINAL_READ',
-      'CLIENT_READ',
-      'STAFFING_AGENCY_CREATE',
-      'STAFFING_AGENCY_DELETE',
-      'STAFFING_AGENCY_READ',
-      'STAFFING_AGENCY_UNDELETE',
-      'STAFFING_AGENCY_UPDATE',
-      'STAFF_MEMBER_CREATE',
-      'STAFF_MEMBER_DELETE',
-      'STAFF_MEMBER_READ',
-      'STAFF_MEMBER_UNDELETE',
-      'STAFF_MEMBER_UPDATE'
-    ]
-      .concat(laborCategories.map(x => x.key).filter(key => key !== 'ADMIN').map(key => `INCOME_${key}_VIEW`))
-      .join(',')
-  },
-  {
-    name: 'Standard User',
-    key: 'USER',
-  }
-].concat(terminals.map(loc => ({
-  name: `${loc.name}`,
-  key: `${loc.key}`,
-  permissions: `TERMINAL_${loc.key}_ACCESS`
-})))
-
-
-
-const initializeTerminals = async db => {
-
-  for (let terminal of terminals) {
-    const record = await db.upsert(db.Terminal, terminal)
-    register(`TERMINAL_${terminal.key}_ACCESS`, `Access ${terminal.name}.`)
-    console.log(`Upserted Terminal ${terminal.name}: ${JSON.stringify(record)}`) 
-  }
-}
-
-const initializeRoles = async db => {
-  for (let role of roles) {
-    const record = await db.upsert(db.Role, role)
-    console.log(`Upserted Role ${role.name}: ${JSON.stringify(record)}`)
-  }
-}
-
-const initializeLaborCategories = async db => {
-  for (let category of laborCategories) {
-    const record = await db.upsert(db.LaborCategory, category)
-    register(`INCOME_${category.key}_VIEW`, `View ${category.name} Staff income.`)
-    console.log(`Upserted Labor Category ${category.name}: ${JSON.stringify(record)}`) 
-  }
-}
-
-const initializeServiceCategories = async db => {
-  for (let category of serviceCategories) {
-    const record = await db.upsert(db.ServiceCategory, category)
-    const laborCategories = await db.LaborCategory.findAll({
-      where: {
-        key: {
-          [Op.in]: category.laborCategories
-        }
-      }
-    })
-    await record.setLaborCategories(laborCategories)
-
-    console.log(`Upserted Service Category ${category.name}: ${JSON.stringify(record)}`) 
-  }
-}
+const chalk = require('chalk')
 
+const { prod, dev } = require('./seeds')
 const init = async (db) => {
-  await initializeTerminals(db)
-  await initializeRoles(db)
-  await initializeLaborCategories(db)
-  await initializeServiceCategories(db)
+  console.log(chalk.cyan('Initializing prod records'))
+  await prod(db)
+  if (process.env.NODE_ENV === 'development') {
+    console.log(chalk.cyan.bold('Initializing DEV records'))
+    await dev(db)
+  }
 }
 
 module.exports = {
-  initializeRoles,
-  initializeLaborCategories,
   init
 }

+ 2 - 0
lib/database/labor.js

@@ -10,8 +10,10 @@ const Labor = sequelize.define('labor', {
   staffMemberId: Sequelize.UUID,
   workdayId: Sequelize.UUID,
   laborCategoryId: Sequelize.UUID,
+  hours: Sequelize.DOUBLE,
   regularHours: Sequelize.DOUBLE,
   overtimeHours: Sequelize.DOUBLE,
+  doubletimeHours: Sequelize.DOUBLE,
   laborCost: Sequelize.DECIMAL(19, 4),
 }, {
   paranoid: true,

+ 26 - 0
lib/database/migrations/1.04-labor-hours.js

@@ -0,0 +1,26 @@
+module.exports = {
+  version: 1.04,
+  name: 'Labor Hours',
+  description: 'Adds an hours field to labor and workday, moves previously separated hours into that field.',
+  up: async (queryInterface, Sequelize) => {
+    await queryInterface.addColumn('labors', 'hours', Sequelize.DOUBLE)
+    await queryInterface.addColumn('workdays', 'hours', Sequelize.DOUBLE)
+    await queryInterface.addColumn('labors', 'doubletimeHours', Sequelize.DOUBLE)
+    await queryInterface.addColumn('workdays', 'doubletimeHours', Sequelize.DOUBLE)
+    await queryInterface.sequelize.query(`
+      UPDATE labors
+      SET hours = COALESCE(overtimeHours, 0) + COALESCE(regularHours, 0),
+        overtimeHours = null,
+        regularHours = null
+      WHERE hours is null
+    `)
+    await queryInterface.sequelize.query(`
+      UPDATE workdays
+      SET hours = COALESCE(overtimeHours, 0) + COALESCE(regularHours, 0),
+        overtimeHours = null,
+        regularHours = null
+      WHERE hours is null
+    `)
+    return 1.04
+  }
+}

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

@@ -3,7 +3,8 @@ module.exports = Object.assign(migrations, {
   'special-dispatch-app': [
     require('./1.01-location-to-terminal.js'),
     require('./1.02-retailer-to-client.js'),
-    require('./1.03-service-categories.js')
+    require('./1.03-service-categories.js'),
+    require('./1.04-labor-hours.js')
   ]
 })
 

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

@@ -0,0 +1,303 @@
+const { dict } = require('@alancnet/material-framework/lib/util')
+
+const users = async (db) => {
+  const C = require('../../controllers')
+  const { User, StaffMember, Terminal, Role, Client } = db
+  
+  const DEVELOPER = await db.upsert(Role, {
+    name: 'Developer',
+    key: 'DEV',
+    permissions: C.auth.permissions.permissions.join(',')
+  })
+
+  const terminals = dict(await Terminal.findAll())
+  const roles = dict(await Role.findAll())
+
+  const [alan] = await db.fill(User, 
+    [
+      {
+        name: 'Alan Colon',
+        email: 'alancnet@gmail.com',
+        password: 'hello',
+        roles: [
+          'MANAGER',
+          'DEV'
+        ]
+      }
+    ],
+    ['email']
+  )
+
+  const [ adam, bear, charlie, dustin, evelynn, frank, gary, harry ] =
+  await db.fill(StaffMember,
+    [
+      {
+        name: 'Adam Alvarez',
+        terminal: 'LAX',
+        laborCategory: 'WAREHOUSE',
+        wage: 15
+      },
+      {
+        name: 'Bear Biggs',
+        terminal: 'LAX',
+        laborCategory: 'ADMIN',
+        wage: 15
+      },
+      {
+        name: 'Charlie Chavez',
+        terminal: 'LAX',
+        laborCategory: 'OPS',
+        wage: 15
+      },
+      {
+        name: 'Dustin Dearly',
+        terminal: 'LAX',
+        laborCategory: 'DELIVERY',
+        wage: 15
+      },
+      {
+        name: 'Evelynn Ebert',
+        terminal: 'LAS',
+        laborCategory: 'WAREHOUSE',
+        wage: 15
+      },
+      {
+        name: 'Frank Fudgley',
+        terminal: 'LAS',
+        laborCategory: 'ADMIN',
+        wage: 15
+      },
+      {
+        name: 'Gary Gospel',
+        terminal: 'LAS',
+        laborCategory: 'OPS',
+        wage: 15
+      },
+      {
+        name: 'Harry Havenworth',
+        terminal: 'LAS',
+        laborCategory: 'DELIVERY',
+        wage: 15
+      },
+    ],
+    ['name']
+  )
+
+  const laxLabor = {
+    workdays: [
+      {
+        labor: [
+          { staffMemberId: adam.id, hours: null },
+          { staffMemberId: bear.id, hours: null },
+          { staffMemberId: charlie.id, hours: null },
+          { staffMemberId: dustin.id, hours: null }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: adam.id, hours: 5 },
+          { staffMemberId: bear.id, hours: 10 },
+          { staffMemberId: charlie.id, hours: 15 },
+          { staffMemberId: dustin.id, hours: 20 }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: adam.id, hours: 5 },
+          { staffMemberId: bear.id, hours: 10 },
+          { staffMemberId: charlie.id, hours: 15 },
+          { staffMemberId: dustin.id, hours: 20 }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: adam.id, hours: 5 },
+          { staffMemberId: bear.id, hours: 10 },
+          { staffMemberId: charlie.id, hours: 15 },
+          { staffMemberId: dustin.id, hours: 20 }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: adam.id, hours: 5 },
+          { staffMemberId: bear.id, hours: 10 },
+          { staffMemberId: charlie.id, hours: 15 },
+          { staffMemberId: dustin.id, hours: 20 }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: adam.id, hours: 5 },
+          { staffMemberId: bear.id, hours: 10 },
+          { staffMemberId: charlie.id, hours: 15 },
+          { staffMemberId: dustin.id, hours: 20 }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: adam.id, hours: null },
+          { staffMemberId: bear.id, hours: null },
+          { staffMemberId: charlie.id, hours: null },
+          { staffMemberId: dustin.id, hours: null }
+        ]
+      }
+    ]
+  }
+
+  const lasLabor = {
+    workdays: [
+      {
+        labor: [
+          { staffMemberId: evelynn.id, hours: null },
+          { staffMemberId: frank.id, hours: null },
+          { staffMemberId: gary.id, hours: null },
+          { staffMemberId: harry.id, hours: null }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: evelynn.id, hours: 5 },
+          { staffMemberId: frank.id, hours: 10 },
+          { staffMemberId: gary.id, hours: 15 },
+          { staffMemberId: harry.id, hours: 20 }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: evelynn.id, hours: 5 },
+          { staffMemberId: frank.id, hours: 10 },
+          { staffMemberId: gary.id, hours: 15 },
+          { staffMemberId: harry.id, hours: 20 }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: evelynn.id, hours: 5 },
+          { staffMemberId: frank.id, hours: 10 },
+          { staffMemberId: gary.id, hours: 15 },
+          { staffMemberId: harry.id, hours: 20 }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: evelynn.id, hours: 5 },
+          { staffMemberId: frank.id, hours: 10 },
+          { staffMemberId: gary.id, hours: 15 },
+          { staffMemberId: harry.id, hours: 20 }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: evelynn.id, hours: 5 },
+          { staffMemberId: frank.id, hours: 10 },
+          { staffMemberId: gary.id, hours: 15 },
+          { staffMemberId: harry.id, hours: 20 }
+        ]
+      },
+      {
+        labor: [
+          { staffMemberId: evelynn.id, hours: null },
+          { staffMemberId: frank.id, hours: null },
+          { staffMemberId: gary.id, hours: null },
+          { staffMemberId: harry.id, hours: null }
+        ]
+      }
+    ]
+  }
+
+  const [ lax, las ] = await db.fill(Client, [
+    {
+      name: 'Los Angeles Total',
+      key: 'LAX',
+      terminal: 'LAX'
+    },
+    {
+      name: 'Las Vegas Total',
+      key: 'LAS',
+      terminal: 'LAS'
+    }
+  ])
+
+  const lasServices = {
+    workdays: [
+      { services: [ { clientId: las.id, cartons: null } ] },
+      { services: [ { clientId: las.id, cartons: 1000 } ] },
+      { services: [ { clientId: las.id, cartons: 1000 } ] },
+      { services: [ { clientId: las.id, cartons: 1000 } ] },
+      { services: [ { clientId: las.id, cartons: 1000 } ] },
+      { services: [ { clientId: las.id, cartons: 1000 } ] },
+      { services: [ { clientId: las.id, cartons: null } ] }
+    ]
+  }
+
+  const laxServices = {
+    workdays: [
+      { services: [ { clientId: lax.id, cartons: null } ] },
+      { services: [ { clientId: lax.id, cartons: 1000 } ] },
+      { services: [ { clientId: lax.id, cartons: 1000 } ] },
+      { services: [ { clientId: lax.id, cartons: 1000 } ] },
+      { services: [ { clientId: lax.id, cartons: 1000 } ] },
+      { services: [ { clientId: lax.id, cartons: 1000 } ] },
+      { services: [ { clientId: lax.id, cartons: null } ] }
+    ]
+  }
+
+
+  for (let week of ['2018-07-08', '2018-07-01', '2018-06-24']) {
+    await C.labor.patch({
+      body: lasLabor,
+      params: {
+        terminal: 'LAS',
+        week
+      },
+    }, {
+      status() { return this },
+      end() { return this }
+    })
+    await C.labor.patch({
+      body: laxLabor,
+      params: {
+        terminal: 'LAX',
+        week
+      },
+    }, {
+      status() { return this },
+      end() { return this }
+    })
+
+    await C.services.patch({
+      body: lasServices,
+      params: {
+        terminal: 'LAS',
+        week
+      },
+    }, {
+      status() { return this },
+      end() { return this }
+    })
+    await C.services.patch({
+      body: laxServices,
+      params: {
+        terminal: 'LAX',
+        week
+      },
+    }, {
+      status() { return this },
+      end() { return this }
+    })
+
+
+  }
+
+
+  console.log('done')
+}
+
+const seed = async (db) => {
+  await users(db)
+}
+
+module.exports = Object.assign(seed, {
+  seed,
+  users
+})

+ 4 - 0
lib/database/seeds/index.js

@@ -0,0 +1,4 @@
+module.exports = {
+  dev: require('./dev'),
+  prod: require('./prod')
+}

+ 211 - 0
lib/database/seeds/prod.js

@@ -0,0 +1,211 @@
+const { Op } = require('sequelize')
+const { controllers: C } = require('@alancnet/material-framework/server')
+const { register } = C.auth.permissions
+const terminals = [
+  {
+    name: 'Las Vegas',
+    key: 'LAS'
+  },
+  {
+    name: 'Los Angeles',
+    key: 'LAX'
+  },
+  {
+    name: 'Phoenix',
+    key: 'PHX'
+  },
+  {
+    name: 'San Francisco',
+    key: 'SFO'
+  }
+]
+
+const laborCategories = [
+  {
+    name: 'Warehouse',
+    key: 'WAREHOUSE'
+  },
+  {
+    name: 'Admin',
+    key: 'ADMIN'
+  },
+  {
+    name: 'Delivery',
+    key: 'DELIVERY'
+  },
+  {
+    name: 'Ops',
+    key: 'OPS'
+  }
+]
+
+const serviceCategories = [
+  {
+    name: 'Inbound',
+    key: 'INBOUND',
+    laborCategories: [
+      'WAREHOUSE',
+      'ADMIN',
+      'OPS'
+    ],
+    displayOrder: 1
+  },
+  {
+    name: 'Delivery',
+    key: 'DELIVERY',
+    laborCategories: [
+      'DELIVERY'
+    ],
+    displayOrder: 2
+  }
+]
+
+const roles = [
+  {
+    name: 'Management',
+    key: 'MANAGER',
+    permissions: [
+      'LABOR_CATEGORY_CREATE',
+      'LABOR_CATEGORY_DELETE',
+      'LABOR_CATEGORY_READ',
+      'LABOR_CATEGORY_UNDELETE',
+      'LABOR_CATEGORY_UPDATE',
+      'LABOR_ENTRY',
+      'LABOR_VIEW',
+      'TERMINAL_ALL_ACCESS',
+      'TERMINAL_CREATE',
+      'TERMINAL_DELETE',
+      'TERMINAL_READ',
+      'TERMINAL_UNDELETE',
+      'TERMINAL_UPDATE',
+      'METRICS_VIEW',
+      'CLIENT_CREATE',
+      'CLIENT_DELETE',
+      'CLIENT_READ',
+      'CLIENT_UNDELETE',
+      'CLIENT_UPDATE',
+      'ROLE_CREATE',
+      'ROLE_DELETE',
+      'ROLE_READ',
+      'ROLE_UNDELETE',
+      'ROLE_UPDATE',
+      'SERVICES_ENTRY',
+      'SERVICES_VIEW',
+      'STAFFING_AGENCY_CREATE',
+      'STAFFING_AGENCY_DELETE',
+      'STAFFING_AGENCY_READ',
+      'STAFFING_AGENCY_UNDELETE',
+      'STAFFING_AGENCY_UPDATE',
+      'STAFF_MEMBER_CREATE',
+      'STAFF_MEMBER_DELETE',
+      'STAFF_MEMBER_READ',
+      'STAFF_MEMBER_UNDELETE',
+      'STAFF_MEMBER_UPDATE',
+      'USER_CREATE',
+      'USER_DELETE',
+      'USER_READ',
+      'USER_UNDELETE',
+      'USER_UPDATE',
+      'INCOME_ADMIN_VIEW',
+      'INCOME_DELIVERY_VIEW',
+      'INCOME_OPS_VIEW',
+      'INCOME_WAREHOUSE_VIEW',
+      'VIEW_TERMINAL_LAS',
+      'VIEW_TERMINAL_LAX',
+      'VIEW_TERMINAL_PHX',
+      'VIEW_TERMINAL_SFO'
+    ].join(',')
+  },
+  {
+    name: 'Terminal Manager',
+    key: 'TERMINAL_MANAGER',
+    permissions: [
+      'LABOR_CATEGORY_READ',
+      'TERMINAL_READ',
+      'METRICS_VIEW',
+      'CLIENT_CREATE',
+      'CLIENT_DELETE',
+      'CLIENT_READ',
+      'CLIENT_UNDELETE',
+      'CLIENT_UPDATE',
+      'CLIENT_VIEW',
+      'STAFFING_AGENCY_READ',
+      'STAFF_MEMBER_CREATE',
+      'STAFF_MEMBER_DELETE',
+      'STAFF_MEMBER_READ',
+      'STAFF_MEMBER_UNDELETE',
+      'STAFF_MEMBER_UPDATE',
+      'STAFF_MEMBER_VIEW'
+    ]
+      .concat(laborCategories.map(x => x.key).filter(key => key !== 'ADMIN').map(key => `INCOME_${key}_VIEW`))
+      .join(',')
+  },
+  {
+    name: 'Accounting',
+    key: 'ACCOUNTING',
+    permissions: [
+      'LABOR_CATEGORY_READ',
+      'TERMINAL_READ',
+      'CLIENT_READ',
+      'STAFFING_AGENCY_CREATE',
+      'STAFFING_AGENCY_DELETE',
+      'STAFFING_AGENCY_READ',
+      'STAFFING_AGENCY_UNDELETE',
+      'STAFFING_AGENCY_UPDATE',
+      'STAFF_MEMBER_CREATE',
+      'STAFF_MEMBER_DELETE',
+      'STAFF_MEMBER_READ',
+      'STAFF_MEMBER_UNDELETE',
+      'STAFF_MEMBER_UPDATE'
+    ]
+      .concat(laborCategories.map(x => x.key).filter(key => key !== 'ADMIN').map(key => `INCOME_${key}_VIEW`))
+      .join(',')
+  },
+  {
+    name: 'Standard User',
+    key: 'USER',
+  }
+].concat(terminals.map(loc => ({
+  name: `${loc.name}`,
+  key: `${loc.key}`,
+  permissions: `TERMINAL_${loc.key}_ACCESS`
+})))
+
+
+
+const initializeTerminals = async db => {
+  await db.fill(db.Terminal, terminals)
+  for (let terminal of terminals) {
+    register(`TERMINAL_${terminal.key}_ACCESS`, `Access ${terminal.name}.`)
+  }
+}
+
+const initializeRoles = async db => {
+  await db.fill(db.Role, roles)
+}
+
+const initializeLaborCategories = async db => {
+  await db.fill(db.LaborCategory, laborCategories)
+  for (let category of laborCategories) {
+    register(`INCOME_${category.key}_VIEW`, `View ${category.name} Staff income.`)
+  }
+}
+
+const initializeServiceCategories = async db => {
+  await db.fill(db.ServiceCategory, serviceCategories)
+}
+
+const seed = async (db) => {
+  await initializeTerminals(db)
+  await initializeRoles(db)
+  await initializeLaborCategories(db)
+  await initializeServiceCategories(db)
+}
+
+module.exports = Object.assign(seed, {
+  seed,
+  initializeTerminals,
+  initializeRoles,
+  initializeLaborCategories,
+  initializeServiceCategories
+})

+ 3 - 0
lib/database/workday.js

@@ -9,8 +9,10 @@ const Workday = sequelize.define('workday', {
   },
   terminalId: Sequelize.UUID,
   date: Sequelize.DATEONLY,
+  hours: Sequelize.DOUBLE,
   regularHours: Sequelize.DOUBLE,
   overtimeHours: Sequelize.DOUBLE,
+  doubletimeHours: Sequelize.DOUBLE,
   laborCost: Sequelize.DECIMAL(19, 4),
   cartons: Sequelize.INTEGER
 
@@ -24,4 +26,5 @@ const Workday = sequelize.define('workday', {
   ]
 })
 
+Workday.keyField = 'date'
 module.exports = Workday

+ 29 - 0
lib/overtime.js

@@ -0,0 +1,29 @@
+const overtime = (hours, priorHours, terminal) => {
+  switch (terminal) {
+    case 'LAX':
+    case 'SFO':
+      return californiaRules(hours)
+      break
+    case 'LAS':
+    case 'PHX':
+      return federalRules(hours, priorHours)
+      break
+    default:
+      throw new Error(`Overtime rules not implemented for ${terminal}.`)
+  }
+}
+
+const californiaRules = (hours) => {
+  const regularHours = Math.min(hours, 8)
+  const overtimeHours = Math.min(Math.max(hours - 8, 0), 4)
+  const doubletimeHours = Math.max(hours - 12, 0)
+  return {regularHours, overtimeHours, doubletimeHours}
+}
+
+const federalRules = (hours, priorHours) => {
+  const regularHours = Math.min(40 - priorHours, hours)
+  const overtimeHours = hours - regularHours
+  return {regularHours, overtimeHours, doubletimeHours: 0}
+}
+
+module.exports = overtime