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

+ 3 - 2
app/assets/index.js

@@ -5,7 +5,8 @@ module.exports = Object.assign(assets, {
   staffingAgencyIcon: require('@alancnet/icomoon-svg/office.svg'),
   staffMemberIcon: require('@alancnet/icomoon-svg/user-tie.svg'),
   locationIcon: require('@alancnet/icomoon-svg/location.svg'),
-  laborIcon: require('@alancnet/icomoon-svg/accessibility.svg'),
+  laborIcon: require('./noun_constructor_593655.svg'),
   crumbIcon: require('@alancnet/material-design-icons/image_ic_navigate_next_48px.svg'),
-  retailerIcon: require('@alancnet/material-design-icons/maps_ic_store_mall_directory_48px.svg')
+  retailerIcon: require('@alancnet/material-design-icons/maps_ic_store_mall_directory_48px.svg'),
+  serviceIcon: require('./noun_service_406879.svg')
 })

Разница между файлами не показана из-за своего большого размера
+ 6 - 0
app/assets/noun_constructor_593655.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
app/assets/noun_service_406879.svg


+ 3 - 1
app/components/index.js

@@ -4,4 +4,6 @@ require('./staff-member-pages')
 require('./staffing-agencies-pages')
 require('./labor-page')
 require('./labor-entry-page')
-require('./breadcrumb')
+require('./breadcrumb')
+require('./services-entry-page')
+require('./services-page')

+ 8 - 6
app/components/labor-entry-page.js

@@ -56,7 +56,7 @@ app.component('appLaborEntryPage', {
     api.staffMemberDictionary().then(staffMembers => {
       this.staffMembers = staffMembers
     })
-    this.promise = api.get(`/api/labor/${$routeParams.location}/${$routeParams.week}`).then(workdays => {
+    this.promise = api.get(`/api/labor/${$routeParams.location}/${$routeParams.week}`).then(({workdays}) => {
       this.model = workdays
       const staffMemberIds = workdays[0].labor.map(x => x.staffMemberId)
       this.staffMemberLabor = staffMemberIds.map((id, i) => ({
@@ -66,12 +66,14 @@ app.component('appLaborEntryPage', {
     })
 
     this.submit = async () => {
-      const model = this.model.map(workday => ({
-        labor: workday.labor.map(labor => ({
-          staffMemberId: labor.staffMemberId,
-          hours: labor.hours || null
+      const model = {
+        workdays: this.model.map(workday => ({
+          labor: workday.labor.map(labor => ({
+            staffMemberId: labor.staffMemberId,
+            hours: labor.hours || null
+          }))
         }))
-      }))
+      }
       try {
         await api.patch(`/api/labor/${$routeParams.location}/${$routeParams.week}`, model)
         $mdToast.showSimple('Labor saved.')

+ 87 - 0
app/components/services-entry-page.js

@@ -0,0 +1,87 @@
+const app = require('../app')
+const moment = require('moment-immutable')
+const { editIcon } = require('../assets')
+
+app.component('appServicesEntryPage', {
+  template: html`
+  <app-user-area>
+    <app-breadcrumb links="[
+      { text: 'Home', link: '/dashboard' },
+      { text: ctrl.locationKey + ' Services', link: '/services/' + ctrl.locationKey },
+      { text: ctrl.startDate.format('L') + ' - ' + ctrl.endDate.format('L'), link: '/services/' + ctrl.locationKey + '/' + ctrl.startDate.format('YYYY-MM-DD') }
+    ]"></app-breadcrumb>
+    <h1>Service Entry</h1>
+    <p>For week of {{::ctrl.startDate.format('LL')}} to {{::ctrl.endDate.format('LL')}}</p>
+    <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%" />
+        </colgroup>
+        <thead md-head>
+          <tr md-row>
+            <th md-column>Client</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>
+          </tr>
+        </thead>
+        <tbody md-body>
+          <tr md-row ng-repeat="sfl in ::ctrl.retailerServices track by sfl.id">
+            <td md-cell>
+              {{::ctrl.retailers[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">
+            </td>
+          </tr>
+        </tbody>
+      </table>
+      <div>
+        <md-button type="submit" class="md-raised md-primary">Submit</md-button>
+      </div>
+    </form>
+  </app-user-area>
+  `,
+  controllerAs: 'ctrl',
+  controller: function(api, $routeParams, weekdays, $mdToast) {
+    this.weekdays = weekdays
+    this.locationKey = $routeParams.location
+    const week = moment($routeParams.week)
+    if (!week.isSame(week.startOf('week'))) throw new Error('Date is not start of week')
+    this.startDate = week
+    this.endDate = week.endOf('week')
+    api.retailerDictionary().then(retailers => {
+      this.retailers = retailers
+    })
+    this.promise = api.get(`/api/services/${$routeParams.location}/${$routeParams.week}`).then(({workdays}) => {
+      this.model = workdays
+      const retailerIds = workdays[0].services.map(x => x.retailerId)
+      this.retailerServices = retailerIds.map((id, i) => ({
+        id,
+        days: this.model.map(wd => wd.services[i])
+      }))
+    })
+
+    this.submit = async () => {
+      const model = {
+        workdays: this.model.map(workday => ({
+          services: workday.services.map(service => ({
+            retailerId: service.retailerId,
+            cartons: service.cartons || null
+          }))
+        }))
+      }
+      try {
+        await api.patch(`/api/services/${$routeParams.location}/${$routeParams.week}`, model)
+        $mdToast.showSimple('Services saved.')
+      } catch (err) {
+        window.err = err
+        console.error(err)
+        $mdToast.showSimple(`Could not save Services: ${err.message || err.statusText || err}`)
+      }
+    }
+  }
+})

+ 66 - 0
app/components/services-page.js

@@ -0,0 +1,66 @@
+const app = require('../app')
+const { editIcon } = require('../assets')
+
+app.component('appServicesPage', {
+  template: html`
+    <app-user-area>
+      <app-breadcrumb links="[
+        { text: 'Home', link: '/dashboard' },
+        { text: ctrl.locationKey + ' Services', link: '/services/' + ctrl.locationKey }
+      ]"></app-breadcrumb>
+
+      <h1>Services</h1>
+      <table md-table ng-model="ctrl.selected" md-progress="ctrl.promise">
+        <thead md-head>
+          <tr md-row>
+            <th md-column>Week Starting</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>
+            <th md-column>Actions</th>
+          </tr>
+        </thead>
+        <tbody md-body>
+          <tr md-row ng-repeat="week in ::ctrl.services track by week.workweek">
+            <td md-cell>
+              {{::week.workweek}}
+            </td>
+            <td md-cell ng-repeat="workday in ::week.workdays track by $index">
+              <div>
+                <span ng-if="::workday" style="white-space: nowrap;">
+                  {{::workday.cartons || 0}}
+                  <span hide show-xs>c</span>
+                  <span hide show-sm>ctn</span>
+                  <span hide show-gt-sm>cartons</span>
+                </span>
+                <span ng-if="::!workday" md-colors="{ color: 'primary-100' }">
+                  N/A
+                </span>
+              </div>
+            </td>
+            <td md-cell>
+              <md-button ng-href="services/{{::ctrl.location.key}}/{{::week.workweek}}">
+                <md-icon md-svg-icon="${editIcon}"></md-icon>
+                Edit
+              </md-button>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+    </app-user-area>
+  `,
+  controllerAs: 'ctrl',
+  controller: function(api, $routeParams, weekdays) {
+    this.locationKey = $routeParams.location
+    this.weekdays = weekdays
+    api.location($routeParams.location).then(location => {
+      this.location = location
+    })
+    this.promise = api.get(`/api/services/${$routeParams.location}`).then(services => {
+      this.services = services
+    })
+  }
+})

+ 8 - 2
app/components/user-area-nav.js

@@ -1,6 +1,6 @@
 const app = require('../app')
 const _ = require('lodash')
-const { dashboardIcon, staffMemberIcon, staffingAgencyIcon, laborIcon, retailerIcon } = require('../assets')
+const { dashboardIcon, staffMemberIcon, staffingAgencyIcon, laborIcon, retailerIcon, serviceIcon } = require('../assets')
 
 app.component('appUserAreaNav', {
   template: html`
@@ -23,6 +23,12 @@ app.component('appUserAreaNav', {
           {{::location.key}} Labor
         </md-button>
       </md-menu-item>
+      <md-menu-item>
+        <md-button ng-href="/services/{{::location.key}}">
+          <md-icon md-svg-icon="${serviceIcon}"></md-icon>
+          {{::location.key}} Services
+        </md-button>
+      </md-menu-item>
       <md-menu-item>
         <md-button ng-href="/staff-members/{{::location.key}}">
           <md-icon md-svg-icon="${staffMemberIcon}"></md-icon>
@@ -60,7 +66,7 @@ app.component('appUserAreaNav', {
   `,
   controllerAs: 'ctrl',
   controller: function(api) {
-    window.api = api.get('/api/locations').then(locations => {
+    window.api = api.locations().then(locations => {
       this.locations = _.sortBy(locations, 'key')
     })
   }

+ 2 - 0
app/routes.js

@@ -4,4 +4,6 @@ module.exports = function($routeProvider) {
   $routeProvider.crudRoutes({ camelName: 'staffingAgency' })
   $routeProvider.when('/labor/:location', {template: '<app-labor-page />'})
   $routeProvider.when('/labor/:location/:week', {template: '<app-labor-entry-page />'})
+  $routeProvider.when('/services/:location', {template: '<app-services-page />'})
+  $routeProvider.when('/services/:location/:week', {template: '<app-services-entry-page />'})
 }

+ 10 - 1
app/services/api.js

@@ -15,10 +15,19 @@ app.run(function(api) {
     (await api.locationsDictionary())[key]
   )
   let staffMembers = null
-  api.staffMembers = () => staffMembers || (staffMembers = api.get('/api/staff-members'))
+  api.staffMembers = () => staffMembers || (staffMembers = api.get('/api/staff-members/all'))
 
   let staffMemberDictionary = null
   api.staffMemberDictionary = () => staffMemberDictionary || (staffMemberDictionary = api.staffMembers().then(staffMembers =>
     _.fromPairs(staffMembers.map(sm => [sm.id, sm]))
   ))
+
+  let retailers = null
+  api.retailers = () => retailers || (retailers = api.get('/api/retailers/all'))
+
+  let retailerDictionary = null
+  api.retailerDictionary = () => retailerDictionary || (retailerDictionary = api.retailers().then(retailers =>
+    _.fromPairs(retailers.map(sm => [sm.id, sm]))
+  ))
+
 })

+ 1 - 1
config.js

@@ -17,6 +17,6 @@ module.exports = {
     port: null,
     storage: 'project.db',
     operatorsAliases: false,
-    logging: false
+    logging: console.log
   }
 }

+ 3 - 1
lib/controllers/index.js

@@ -2,11 +2,13 @@ const statistics = require('./statistics')
 const staffMember = require('./staff-member')
 const staffingAgency = require('./staffing-agency')
 const labor = require('./labor')
+const services = require('./services')
 const { controllers: C } = require('@alancnet/material-framework/server')
 
 module.exports = Object.assign(C, {
   statistics,
   staffMember,
   staffingAgency,
-  labor
+  labor,
+  services
 })

+ 14 - 9
lib/controllers/labor.js

@@ -55,7 +55,7 @@ const get = async (req, res) => {
       locationId: location.id,
       date: {
         [Op.gte]: week,
-        [Op.lt]: moment(week).endOf('week')
+        [Op.lte]: moment(week).endOf('week')
       }
     },
     order: [ 'date' ]
@@ -116,7 +116,9 @@ const get = async (req, res) => {
     return wd
   })
 
-  res.status(200).send(allWorkdays)
+  res.status(200).send({
+    workdays: allWorkdays
+  })
 }
 
 const patch = async (req, res) => {
@@ -190,19 +192,15 @@ const patch = async (req, res) => {
 
     // Update with model
     const model = req.body
-    await Promise.all(model.map(async (modelWorkday, i) => {
+    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]
-      await Promise.all(workday.labor.map(async labor => {
+      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
-        
-        if (labor.regularHours || labor.overtimeHours || !labor.isNewRecord) {
-          await labor.save({ transaction })
-        }
-      }))
+      })
       // TODO: Update laborCost when wages are in
       workday.overtimeHours = workday.labor.map(l => l.overtimeHours || 0).reduce((a, b) => a + b, 0)
       workday.regularHours = workday.labor.map(l => l.regularHours || 0).reduce((a, b) => a + b, 0)
@@ -217,6 +215,13 @@ const patch = async (req, res) => {
       }
 
     }))
+
+    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()

+ 229 - 0
lib/controllers/services.js

@@ -0,0 +1,229 @@
+const _ = require('lodash')
+const moment = require('moment-immutable')
+const { getWeeks, formatDate, parseDate } = require('../dates')
+const { Retailer, Workday, Location, Service, sequelize } = require('../database')
+const { Op } = require('sequelize')
+
+const list = async (req, res) => {
+  const locationKey = req.params.location
+  const location = await Location.findOne({where: {key: locationKey}})
+  if (!location) return res.status(404).end()
+  const workdays = await Workday.findAll({where: { locationId: location.id }})
+
+  let workweeks = _.groupBy(workdays, d => formatDate(moment(d.date).startOf('week')))
+  
+  // fill workweeks
+  getWeeks(10).forEach(week => {
+    const w = formatDate(week)
+    if (!workweeks[w]) workweeks[w] = []
+  })
+
+  // fill workdays
+  workweeks = _.chain(workweeks)
+    .toPairs()
+    .map(([w, days]) => {
+      days  = _.chain(days)
+        .map(d => [moment(d.date).weekday(), d])
+        .fromPairs()
+        .value()
+      
+      days = _.range(0, 7).map(d => days[d] || null)
+        .map(day => day && {
+          cartons: day.cartons
+        })
+
+      return {
+        workweek: w,
+        workdays: days
+      }
+    })
+    .sortBy(x => x.workweek)
+    .reverse()
+    .value()
+
+  res.status(200).send(workweeks)
+}
+
+const get = async (req, res) => {
+  const locationKey = req.params.location
+  const location = await Location.findOne({where: {key: locationKey}})
+  const week = parseDate(req.params.week)
+  const workdays = await Workday.findAll({
+    where: {
+      locationId: location.id,
+      date: {
+        [Op.gte]: week,
+        [Op.lt]: moment(week).endOf('week')
+      }
+    },
+    order: [ 'date' ]
+  })
+  const services = await Service.findAll({
+    where: {
+      workdayId: {
+        [Op.in]: workdays.map(x => x.id)
+      }
+    }
+  })
+  const servicesByWorkday = _.groupBy(services, x => x.workdayId)
+  const extraRetailers = _.chain(services)
+    .map(x => x.retailerId)
+    .filter(x => x)
+    .uniq()
+    .value()
+  const retailers = await Retailer.findAll({
+    where: {
+      [Op.or]: [
+        { locationId: location.id },
+        {
+          id: {
+            [Op.in]: extraRetailers
+          }
+        }
+      ]      
+    },
+    order: [ 'name' ]
+  })
+  const retailersById = _.chain(retailers).map(x => [x.id, x]).fromPairs().value()
+
+  // Fill in empty days
+  const workdaysByKey = _.chain(workdays).map(wd => [formatDate(wd.date), wd]).fromPairs().value()
+  const allWorkdays = []
+  for (let day = week, i = 0; i < 7; i++, day = day.add(1, 'day')) {
+    const wd = workdaysByKey[formatDate(day)]
+    allWorkdays[i] = wd ? wd.toJSON() : {}
+  }
+
+  const workdaysWithServices = allWorkdays.map(wd => {
+    const services = servicesByWorkday[wd.id] || []
+    const serviceByRetailer = _.chain(services).map(l => [l.retailerId, l]).fromPairs().value()
+    // Map from staffMembers to preserve sorting
+    wd.services = retailers
+      .map(sm => serviceByRetailer[sm.id] || {retailerId: sm.id})
+      .map(sm => ({
+        retailerId: sm.retailerId,
+        cartons: sm.cartons || null
+      }))
+    // Restore any staffMembers that are no longer assigned this location
+    services.forEach(l => {
+      if (!serviceByRetailer[l.retailerId]) {
+        wd.services.push(l)
+      }
+    })
+    return wd
+  })
+
+  res.status(200).send({
+    workdays: allWorkdays
+  })
+}
+
+const patch = async (req, res) => {
+  const transaction = await sequelize.transaction()
+  try {
+    const locationKey = req.params.location
+    const location = await Location.findOne({where: {key: locationKey}})
+    const week = parseDate(req.params.week)
+    const workdays = await Workday.findAll({
+      where: {
+        locationId: location.id,
+        date: {
+          [Op.gte]: week,
+          [Op.lte]: moment(week).endOf('week')
+        }
+      },
+      order: [ 'date' ]
+    })
+    const services = await Service.findAll({
+      where: {
+        workdayId: {
+          [Op.in]: workdays.map(x => x.id)
+        }
+      }
+    })
+    const servicesByWorkday = _.groupBy(services, x => x.workdayId)
+    const extraRetailers = _.chain(services)
+      .map(x => x.retailerId)
+      .filter(x => x)
+      .uniq()
+      .value()
+    const retailers = await Retailer.findAll({
+      where: {
+        [Op.or]: [
+          { locationId: location.id },
+          {
+            id: {
+              [Op.in]: extraRetailers
+            }
+          }
+        ]      
+      },
+      order: [ 'name' ]
+    })
+    const retailersById = _.chain(retailers).map(x => [x.id, x]).fromPairs().value()
+
+    // Fill in empty days
+    const workdaysByKey = _.chain(workdays).map(wd => [formatDate(wd.date), wd]).fromPairs().value()
+    const allWorkdays = []
+    for (let day = week, i = 0; i < 7; i++, day = day.add(1, 'day')) {
+      const wd = workdaysByKey[formatDate(day)]
+      allWorkdays[i] = wd || Workday.build({
+        locationId: location.id,
+        date: week.add(i, 'day')
+      })
+    }
+
+    const workdaysWithServices = allWorkdays.map(wd => {
+      const services = servicesByWorkday[wd.id] || []
+      const servicesByRetailer = _.chain(services).map(l => [l.retailerId, l]).fromPairs().value()
+      // Map from staffMembers to preserve sorting
+      wd.services = retailers.map(sm => servicesByRetailer[sm.id] || Service.build({retailerId: sm.id, workdayId: wd.id}))
+      // Restore any staffMembers that are no longer assigned this location
+      services.forEach(l => {
+        if (!retailersById[l.retailerId]) {
+          wd.services.push(l)
+        }
+      })
+      return wd
+    })
+
+    // Update with model
+    const model = req.body
+    await Promise.all(model.workdays.map(async (modelWorkday, i) => {
+      const modelServicesById = _.chain(modelWorkday.services).map(l => [l.retailerId, l]).fromPairs().value()
+      const workday = allWorkdays[i]
+
+      workday.services.forEach(service => {
+        const modelService = modelServicesById[service.retailerId]
+
+        service.cartons = modelService.cartons || 0
+      })
+
+      workday.cartons = workday.services.map(l => l.cartons).reduce((a, b) => a + b, 0)
+      
+      if (workday.cartons || !workday.isNewRecord) {
+        await workday.save({ transaction })
+      }
+
+      await Promise.all(workday.services.map(async service => {
+        if (service.cartons || !service.isNewRecord) {
+          await service.save({ transaction })
+        }
+      }))
+    }))
+    await transaction.commit()
+    
+    res.status(200).end()
+
+  } catch (err) {
+    await transaction.rollback()
+    console.log(err.message)
+    console.log(err.sql)
+    throw err
+  }
+}
+module.exports = {
+  list,
+  get,
+  patch
+}

+ 2 - 1
lib/database/service.js

@@ -11,7 +11,8 @@ const Service = sequelize.define('service', {
   retailerId: Sequelize.UUID,
   date: Sequelize.DATEONLY, // Copy from Workday
   delivered: Sequelize.INTEGER,
-  scanned: Sequelize.INTEGER
+  scanned: Sequelize.INTEGER,
+  cartons: Sequelize.INTEGER
 }, {
   paranoid: true,
   indexes: [

+ 1 - 0
lib/database/workday.js

@@ -12,6 +12,7 @@ const Workday = sequelize.define('workday', {
   regularHours: Sequelize.DOUBLE,
   overtimeHours: Sequelize.DOUBLE,
   laborCost: Sequelize.DECIMAL,
+  cartons: Sequelize.INTEGER
 
 }, {
   paranoid: true,

+ 8 - 0
lib/routes.js

@@ -6,7 +6,15 @@ module.exports = app => {
   crudRoutes({ app, apiPrefix: '/api/staff-members/:location', controller: C.staffMember, camelName: 'staffMember' })
  // crudRoutes({ app, controller: C.staffMember, camelName: 'staffMember' })
   crudRoutes({ app, controller: C.staffingAgency, camelName: 'staffingAgency' })
+
+  // Labor
   app.get('/api/labor/:location', C.labor.list)
   app.get('/api/labor/:location/:week', C.labor.get)
   app.patch('/api/labor/:location/:week', C.labor.patch)
+
+  // Services
+  app.get('/api/services/:location', C.services.list)
+  app.get('/api/services/:location/:week', C.services.get)
+  app.patch('/api/services/:location/:week', C.services.patch)
+
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов