Browse Source

Statistics

Alan Colon 7 years ago
parent
commit
2492cbc9ba

+ 3 - 0
app/api-service.js

@@ -1,4 +1,5 @@
 const app = require('./app')
+const appApiService = require('./app-api-service')
 
 app.service('api', function($http) {
   let opts = {
@@ -24,4 +25,6 @@ app.service('api', function($http) {
     trash: () => api($http.get(`${apiPrefix}/trash`, opts)),
     undelete: (id) => api($http.delete(`${apiPrefix}/trash/${id}`, opts))
   })
+
+  appApiService.apply(this, [api, $http])
 })

+ 3 - 0
app/app-api-service.js

@@ -0,0 +1,3 @@
+module.exports = function(api, $http) {
+  this.statistics = () => api($http.get('/api/statistics'))
+}

+ 5 - 4
app/app-index.js

@@ -1,16 +1,17 @@
 const app = require('./app')
+require('./statistics')
 // TODO: Register app specific services
 app.config(($routeProvider, $mdThemingProvider) => {
   // TODO: App Title
-  window.title = 'My App'
+  window.title = 'Special Dispatch'
 
   // TODO: Select a theme
   const palettes = ['red', 'pink', 'purple', 'deep-purple', 'indigo', 'blue', 'light-blue', 'cyan', 'teal', 'green', 'light-green', 'lime', 'yellow', 'amber', 'orange', 'deep-orange', 'brown', 'grey', 'blue-grey']
   const randomPalette = () => palettes[Math.floor(Math.random() * palettes.length)]
   $mdThemingProvider.theme('default')
-    .primaryPalette(randomPalette())
-    .accentPalette(randomPalette())
-    .warnPalette(randomPalette())
+    .primaryPalette('blue-grey')
+    .accentPalette('indigo')
+    .warnPalette('red')
 
   // TODO: App Routes
   //  $routeProvider.when('/test', {template: '<app-test-page />'})

+ 2 - 1
app/app.js

@@ -6,6 +6,7 @@ require('angular-route')
 require('angular-material')
 require('angular-material-data-table')
 require('./async')
+require('angular-chart.js')
 window.jQuery = require('jquery')
 
 const es6Html = require('es6-string-html-template')
@@ -13,7 +14,7 @@ window.html = es6Html.html
 window.raw = es6Html.raw
 window.encode = es6Html.encode
 
-const app = angular.module('app', ['ngRoute', 'ngMaterial', 'async', 'md.data.table'])
+const app = angular.module('app', ['ngRoute', 'ngMaterial', 'async', 'md.data.table', 'chart.js'])
 
 app.config(($routeProvider, $locationProvider, $mdThemingProvider) => {
   routes($routeProvider)

+ 4 - 1
app/assets/app-index.js

@@ -1 +1,4 @@
-// TODO: App specific assets
+// TODO: App specific assets
+module.exports = {
+  logo: require('./logo-simple.png')
+}

BIN
app/assets/logo-simple.png


+ 2 - 1
app/components/app-index.js

@@ -1 +1,2 @@
-// TODO: App specific components
+// TODO: App specific components
+require('./app-user-area-nav')

+ 17 - 0
app/components/app-user-area-nav.js

@@ -0,0 +1,17 @@
+const app = require('../app')
+const { dashboardIcon } = require('../assets')
+
+app.component('appUserAreaNav', {
+  template: html`
+    <h3>
+      Intelligence
+    </h3>
+
+    <md-menu-item>
+      <md-button ng-href="/dashboard">
+        <md-icon md-svg-icon="${dashboardIcon}"></md-icon>
+        Dashboard
+      </md-button>
+    </md-menu-item>
+  `
+})

+ 38 - 3
app/components/dashboard-page.js

@@ -3,11 +3,46 @@ const app = require('../app')
 app.component('appDashboardPage', {
   template: html`
     <app-user-area>
-      Dashboard goes here
+      <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-list>
     </app-user-area>
   `,
   controllerAs: 'dashboard',
-  controller: function() {
-
+  controller: function(statistics) {
+    statistics.efficiency().then(statistics => {
+      Object.assign(this, statistics)
+    })
   }
 })

+ 51 - 0
app/statistics.js

@@ -0,0 +1,51 @@
+const _ = require('lodash')
+const app = require('./app')
+
+app.service('statistics', function(api) {
+  const chart = ({
+    rows,
+    seriesField,
+    dataField,
+    labelsField
+  }) => {
+    const labels = _.chain(rows).map(x => x[labelsField]).uniq().value()
+    const series = _.chain(rows).map(x => x[seriesField]).uniq().value()
+    const seriesData = _.chain(rows)
+      .groupBy(x => x[seriesField])
+      .toPairs()
+      .map(([key, rows]) => [
+        key,
+        _.chain(rows)
+          .map(row => [row[labelsField], row[dataField]])
+          .fromPairs()
+          .value()
+      ])
+      .fromPairs()
+      .value()
+    data = series.map(s => {
+      const sdata = seriesData[s]
+      return labels.map(l => sdata[l])
+    })
+
+    return {labels, series, data}
+  }
+
+
+  this.efficiency = async () => {
+    const rows = await api.statistics()
+    return {
+      delivery: chart({
+        rows,
+        seriesField: 'key',
+        dataField: 'delivered',
+        labelsField: 'date'
+      }),
+      efficiency: chart({
+        rows,
+        seriesField: 'key',
+        dataField: 'efficiency',
+        labelsField: 'date'
+      })
+    }
+  }
+})

+ 7 - 0
bin/project.js

@@ -7,6 +7,7 @@ const chalk = require('chalk')
 const asTable = require('as-table')
 const config = require('../config')
 const server = require('../lib/server')
+const xlsxReports = require('../lib/integration/xlsx-reports')
 
 const vorpal = new Vorpal()
 const main = async () => {
@@ -29,12 +30,18 @@ const main = async () => {
   vorpal.command('server', 'Runs the web server')
   .action(() => server.start())
 
+  vorpal.command('import <location> <xlsx>', 'Import an XLSX file')
+  .action(async model => {
+    await xlsxReports.import(model.location, model.xlsx)
+  })
+
   vorpal.delimiter('project>')
   if (process.argv.length > 2) {
     await vorpal.parse(process.argv)
   } else {
     await vorpal.show()
   }
+
 }
 
 main().catch(err => {

+ 5 - 0
lib/app-routes.js

@@ -0,0 +1,5 @@
+const asyncHandler = require('express-async-handler')
+const C = require('./controllers')
+module.exports = (app) => {
+  app.get('/api/statistics', C.statistics.get)
+}

+ 3 - 0
lib/controllers/app-index.js

@@ -0,0 +1,3 @@
+module.exports = {
+  statistics: require('./statistics')
+}

+ 3 - 2
lib/controllers/index.js

@@ -1,4 +1,5 @@
-module.exports = {
+const appIndex = require('./app-index')
+module.exports = Object.assign({
   auth: require('./auth'),
   user: require('./user')
-}
+}, appIndex)

+ 22 - 0
lib/controllers/statistics.js

@@ -0,0 +1,22 @@
+const sequelize = require('../database/sequelize')
+
+const get = async (req, res) => {
+  const [results, metadata] = await sequelize.query(`
+    SELECT
+      loc.key,
+      wd.date,
+      SUM(svc.delivered) as delivered,
+      SUM(svc.scanned) as scanned,
+      SUM(svc.delivered) / (wd.regularHours + (wd.overtimeHours * 1.5)) as efficiency
+    FROM workdays wd
+    JOIN locations loc on wd.locationId = loc.id
+    JOIN services svc on svc.workdayId = wd.id
+    GROUP BY loc.key, wd.date      
+  `)
+  res.status(200).send(results)
+
+}
+
+module.exports = {
+  get
+}

+ 28 - 1
lib/database/app-index.js

@@ -1 +1,28 @@
-// TODO: App Specific Models
+// TODO: App Specific Models
+const Location = require('./location')
+const Workday = require('./workday')
+const Service = require('./service')
+const Retailer = require('./retailer')
+
+const LocationWorkday = Location.hasMany(Workday)
+const WorkdayLocation = Workday.belongsTo(Location)
+
+const WorkdayService = Workday.hasMany(Service)
+const ServiceWorkday = Service.belongsTo(Workday)
+
+const RetailerService = Retailer.hasMany(Service)
+const ServiceRetailer = Service.belongsTo(Retailer)
+
+
+module.exports = {
+  Location,
+  Workday,
+  Service,
+  Retailer,
+  LocationWorkday,
+  WorkdayLocation,
+  WorkdayService,
+  ServiceWorkday,
+  RetailerService,
+  ServiceRetailer,
+}

+ 27 - 0
lib/database/location.js

@@ -0,0 +1,27 @@
+const Sequelize = require('sequelize')
+const sequelize = require('./sequelize')
+
+const Location = sequelize.define('location', {
+  id: {
+    type: Sequelize.UUID,
+    defaultValue: Sequelize.UUIDV1,
+    primaryKey: true
+  },
+  name: Sequelize.STRING,
+  address: Sequelize.STRING,
+  key: {
+    type: Sequelize.STRING,
+    unique: true
+  },
+
+}, {
+  paranoid: true,
+  indexes: [
+    {
+      unique: true,
+      fields: ['key']
+    }
+  ]
+})
+
+module.exports = Location

+ 28 - 0
lib/database/retailer.js

@@ -0,0 +1,28 @@
+const Sequelize = require('sequelize')
+const sequelize = require('./sequelize')
+
+const Retailer = sequelize.define('retailer', {
+  id: {
+    type: Sequelize.UUID,
+    defaultValue: Sequelize.UUIDV1,
+    primaryKey: true
+  },
+  name: Sequelize.STRING,
+  key: {
+    type: Sequelize.STRING,
+    unique: true
+  },
+  address: Sequelize.STRING,
+  locationId: Sequelize.UUID,
+  distanceMiles: Sequelize.DOUBLE
+}, {
+  paranoid: true,
+  indexes: [
+    {
+      unique: true,
+      fields: ['key']
+    }
+  ]
+})
+
+module.exports = Retailer

+ 25 - 0
lib/database/service.js

@@ -0,0 +1,25 @@
+const Sequelize = require('sequelize')
+const sequelize = require('./sequelize')
+
+const Service = sequelize.define('service', {
+  id: {
+    type: Sequelize.UUID,
+    defaultValue: Sequelize.UUIDV1,
+    primaryKey: true
+  },
+  workdayId: Sequelize.UUID,
+  retailerId: Sequelize.UUID,
+  date: Sequelize.DATEONLY, // Copy from Workday
+  delivered: Sequelize.INTEGER,
+  scanned: Sequelize.INTEGER
+}, {
+  paranoid: true,
+  indexes: [
+    {
+      unique: true,
+      fields: ['workdayId', 'retailerId']
+    }
+  ]
+})
+
+module.exports = Service

+ 26 - 0
lib/database/workday.js

@@ -0,0 +1,26 @@
+const Sequelize = require('sequelize')
+const sequelize = require('./sequelize')
+
+const Workday = sequelize.define('workday', {
+  id: {
+    type: Sequelize.UUID,
+    defaultValue: Sequelize.UUIDV1,
+    primaryKey: true
+  },
+  locationId: Sequelize.UUID,
+  date: Sequelize.DATEONLY,
+  regularHours: Sequelize.DOUBLE,
+  overtimeHours: Sequelize.DOUBLE,
+  laborCost: Sequelize.DECIMAL,
+
+}, {
+  paranoid: true,
+  indexes: [
+    {
+      unique: true,
+      fields: ['locationId', 'date']
+    }
+  ]
+})
+
+module.exports = Workday

+ 22 - 0
lib/integration/locations.js

@@ -0,0 +1,22 @@
+const _ = require('lodash')
+const { init, Location } = require('../database')
+
+module.exports = async () => {
+
+  await Location.upsert({
+    name: 'Los Angeles',
+    key: 'LAX'
+  }),
+  await Location.upsert({
+    name: 'San Francisco',
+    key: 'SFO'
+  }),
+  await Location.upsert({
+    name: 'Las Vegas',
+    key: 'LAS'
+  })
+  return _.chain(await Location.findAll())
+    .map(o => [o.key, o])
+    .fromPairs()
+    .value()
+}

+ 101 - 0
lib/integration/xlsx-reports.js

@@ -0,0 +1,101 @@
+const _ = require('lodash')
+const { init, Retailer, Service, Workday } = require('../database')
+const { title } = require('change-case')
+const locations = require('./locations')
+
+const XLSX = require('xlsx')
+
+const $import = async (locKey, filename) => {
+  await init()
+  const locs = await locations()
+  const location = locs[locKey]
+  const book = XLSX.readFile(filename)
+  const sheets = []
+  for (let sheetName of book.SheetNames) {
+    let sheet = book.Sheets[sheetName]
+    const data = XLSX.utils.sheet_to_json(sheet, {
+      header: 1,
+      blankrows: false
+    })
+    sheets.push(data)    
+  }
+  const workdays = []
+  for (let sheet of sheets) {
+    if (!sheet[0] || !sheet[0][0]) continue
+    const date = sheet[0][0]
+    const regularHours = +sheet[5][1]
+    const overtimeHours = +sheet[5][2]
+    let foundRetailer = 0
+    const services = sheet
+      .filter(row => (row[0] && foundRetailer) || (row[0] === 'Retailer' && foundRetailer++))
+      .map(([retailer, totalDel, nonDlv, early, late, onTime, pctOnTime, delivered, scanned, notScanned, pctScanned]) => ({
+        date,
+        retailer,
+        delivered: +delivered,
+        scanned: +scanned
+      }))
+      //.filter(row => row.delivered || row.scanned)
+      workdays.push({
+        date,
+        services,
+        regularHours,
+        overtimeHours
+      })
+  }
+
+  const retailerKeys = _.chain(workdays)
+    .map(x => x.services)
+    .flatten()
+    .map(x => x.retailer)
+    .uniq()
+    .value()
+
+  for (let key of retailerKeys) {
+    console.log(key)
+    await Retailer.upsert({
+      key,
+      name: title(key)
+    })
+  }
+
+  const retailerIds = _.chain(await Retailer.findAll())
+    .map(({key, id}) => [key, id])
+    .fromPairs()
+    .value()
+
+  console.log(retailerIds)
+  
+  for (let workday of workdays) {
+    await Workday.upsert({
+      locationId: location.id,
+      date: workday.date,
+      regularHours: workday.regularHours,
+      overtimeHours: workday.overtimeHours
+    })
+
+    const wd = await Workday.find({where: {
+      locationId: location.id,
+      date: workday.date
+    }})
+
+    for (let service of workday.services) {
+      if (service.delivered || service.scanned) {
+        console.log(service)
+        await Service.upsert({
+          workdayId: wd.id,
+          retailerId: retailerIds[service.retailer],
+          date: workday.date,
+          delivered: service.delivered,
+          scanned: service.scanned
+        })
+      }
+    }
+  }
+
+}
+
+
+
+module.exports = {
+  import: $import
+}

+ 2 - 0
lib/routes.js

@@ -1,8 +1,10 @@
 const crudRoute = require('./crud-route')
 const asyncHandler = require('express-async-handler')
 const C = require('./controllers')
+const appRoutes = require('./app-routes')
 
 module.exports = app => {
+  appRoutes(app)
   app.post('/api/auth/login', asyncHandler(C.auth.login.post))
   crudRoute({ app, controller: C.user, typeName: 'User' })
 }

+ 136 - 0
package-lock.json

@@ -69,6 +69,15 @@
         "acorn": "^5.0.0"
       }
     },
+    "adler-32": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz",
+      "integrity": "sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU=",
+      "requires": {
+        "exit-on-epipe": "~1.0.1",
+        "printj": "~1.1.0"
+      }
+    },
     "aguid": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/aguid/-/aguid-2.0.0.tgz",
@@ -121,6 +130,15 @@
       "resolved": "https://registry.npmjs.org/angular-aria/-/angular-aria-1.7.0.tgz",
       "integrity": "sha512-Va2FxLqXRfNGHd7BO49PrAPVUKo3k8bIpfNDp+TF+e2W14hfiyb0CxLdg+jgUatzIsg/+dCWnuEhCd062XOQvg=="
     },
+    "angular-chart.js": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/angular-chart.js/-/angular-chart.js-1.1.1.tgz",
+      "integrity": "sha1-SfDhjQgXYrbUyXkeSHr/L7sw9a4=",
+      "requires": {
+        "angular": "1.x",
+        "chart.js": "2.3.x"
+      }
+    },
     "angular-material": {
       "version": "1.1.9",
       "resolved": "https://registry.npmjs.org/angular-material/-/angular-material-1.1.9.tgz",
@@ -2241,6 +2259,22 @@
       "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
       "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
     },
+    "cfb": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.0.7.tgz",
+      "integrity": "sha512-KjjZFR+a/e8RDdDTr4PwR0P/HIFRI3sxArFQttml0pFkhIO4TnvS/1+dqtGXPqe5/0MHp2IzjFx1JTzmohHT+w==",
+      "requires": {
+        "commander": "^2.14.1",
+        "printj": "~1.1.2"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.15.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+          "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag=="
+        }
+      }
+    },
     "chai": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz",
@@ -2296,6 +2330,39 @@
       "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
       "dev": true
     },
+    "chart.js": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.3.0.tgz",
+      "integrity": "sha1-QEYOSOLEF8BfwzJc2E97AA3H19Y=",
+      "requires": {
+        "chartjs-color": "^2.0.0",
+        "moment": "^2.10.6"
+      }
+    },
+    "chartjs-color": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.2.0.tgz",
+      "integrity": "sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=",
+      "requires": {
+        "chartjs-color-string": "^0.5.0",
+        "color-convert": "^0.5.3"
+      },
+      "dependencies": {
+        "color-convert": {
+          "version": "0.5.3",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
+          "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0="
+        }
+      }
+    },
+    "chartjs-color-string": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz",
+      "integrity": "sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==",
+      "requires": {
+        "color-name": "^1.0.0"
+      }
+    },
     "check-error": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
@@ -2550,6 +2617,22 @@
       "version": "git+ssh://git@git.alanc.net:10222/alancnet/codebase.git#bd151e08797a2f14a1670d36f4e50b3b0b05ca0a",
       "from": "git+ssh://git@git.alanc.net:10222/alancnet/codebase.git"
     },
+    "codepage": {
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.13.1.tgz",
+      "integrity": "sha512-KnY6cQlgkfBjplnQkLk3M5KEfAKa7i9SMqXp+bMuy2/bgYovvU4LDAQvkMaoScwhvozA9VUtgnbS4Z8f7zVA8w==",
+      "requires": {
+        "commander": "~2.14.1",
+        "exit-on-epipe": "~1.0.1"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.14.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
+          "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw=="
+        }
+      }
+    },
     "collection-visit": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@@ -2768,6 +2851,15 @@
         "vary": "^1"
       }
     },
+    "crc-32": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
+      "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
+      "requires": {
+        "exit-on-epipe": "~1.0.1",
+        "printj": "~1.1.0"
+      }
+    },
     "create-ecdh": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.1.tgz",
@@ -3428,6 +3520,11 @@
       "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
       "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g="
     },
+    "exit-on-epipe": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
+      "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw=="
+    },
     "expand-brackets": {
       "version": "2.1.4",
       "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -4000,6 +4097,11 @@
       "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
       "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
     },
+    "frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="
+    },
     "fragment-cache": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@@ -10992,6 +11094,11 @@
       "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.39.tgz",
       "integrity": "sha1-wRcMt22FjxmGqmB8r+9FhXQDoNs="
     },
+    "printj": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
+      "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ=="
+    },
     "private": {
       "version": "0.1.8",
       "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
@@ -12630,6 +12737,14 @@
         }
       }
     },
+    "ssf": {
+      "version": "0.10.2",
+      "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.10.2.tgz",
+      "integrity": "sha512-rDhAPm9WyIsY8eZEKyE8Qsotb3j/wBdvMWBUsOhJdfhKGLfQidRjiBUV0y/MkyCLiXQ38FG6LWW/VYUtqlIDZQ==",
+      "requires": {
+        "frac": "~1.1.2"
+      }
+    },
     "sshpk": {
       "version": "1.14.1",
       "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz",
@@ -14102,6 +14217,27 @@
         "slide": "^1.1.5"
       }
     },
+    "xlsx": {
+      "version": "0.13.0",
+      "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.13.0.tgz",
+      "integrity": "sha512-TD2NM86NaxshpggGaN5RwU+lyVbg70a/P67L9P7xScvZPVO/MNPwdqOY5hMWAPZUnWZ9NZwFGi1yn35nLCedPw==",
+      "requires": {
+        "adler-32": "~1.2.0",
+        "cfb": "~1.0.7",
+        "codepage": "~1.13.0",
+        "commander": "~2.15.1",
+        "crc-32": "~1.2.0",
+        "exit-on-epipe": "~1.0.1",
+        "ssf": "~0.10.2"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.15.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+          "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag=="
+        }
+      }
+    },
     "xtend": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",

+ 3 - 1
package.json

@@ -18,6 +18,7 @@
     "angular": "^1.6.10",
     "angular-animate": "^1.7.0",
     "angular-aria": "^1.7.0",
+    "angular-chart.js": "^1.1.1",
     "angular-material": "^1.1.9",
     "angular-material-data-table": "^0.10.10",
     "angular-messages": "^1.7.0",
@@ -42,7 +43,8 @@
     "password-prompt": "^1.0.4",
     "plural": "^1.1.0",
     "sequelize": "^4.37.6",
-    "vorpal": "^1.12.0"
+    "vorpal": "^1.12.0",
+    "xlsx": "^0.13.0"
   },
   "devDependencies": {
     "babel-loader": "^7.1.4",

+ 2 - 2
webpack.config.js

@@ -38,13 +38,13 @@ module.exports = {
         use:['style-loader','css-loader', 'sass-loader']
       },
       {
-        test:/\.(?:svg|eot|woff2|woff|ttf)$/,
+        test:/\.(?:svg|png|eot|woff2|woff|ttf)$/,
         use:['file-loader']
       },
       {
         test:/\.html$/,
         use:['html-loader']
-      },
+      }
     ]
   }
 };