Browse Source

Add recommendations; Add Readme

Weiming Zhuang 3 years ago
parent
commit
501395ec3f

+ 16
- 0
Demo.md View File

@@ -0,0 +1,16 @@
1
+#Demo
2
+
3
+1. Login using purchasing crew --> Purchasing dashboard
4
+2. Purchasing Dashboard contents: 
5
+  - To purchase new
6
+  - To reorder
7
+  - Handle adjustments
8
+3. To purchase new / reorder:
9
+  - Auto fill recommended items
10
+  - Show demand forecast
11
+  - Create a new purchase order
12
+
13
+4. Inbound:
14
+  - Receive
15
+  - Check
16
+  - Store

+ 32
- 0
README.md View File

@@ -1 +1,33 @@
1 1
 # Ecommerce Inventory System
2
+
3
+## Products
4
+List of products managed in the inventory system.
5
+
6
+## Variants
7
+Each product has several variants, varied by size or color.
8
+
9
+## Purchasing
10
+
11
+### Purchasing dashboard
12
+- Data: total purchase units, total cost, avg unit cost
13
+- Charts: demand forecast, incoming stocks
14
+- Recommendations: 
15
+  - to create new purchase based on demand forecast
16
+  - to reorder variants based on demand forecast and low inventory
17
+- To handle adjustment from inbound process: receiving, checking, storing
18
+
19
+## Inbound
20
+- Receive: 
21
+  - Partial receive
22
+  - Adjust problems in receiving process
23
+  
24
+- Check: 
25
+  - Partial check
26
+  - Adjust problems in checking process
27
+  - Smart allocation of goods after checking
28
+  
29
+- Store:
30
+  - Warehouse map to locate the storing location
31
+  
32
+## Warehouse Map
33
+- Check the goods stored in each location of warehouse.

+ 10
- 0
WebContent/src/data/recommendations.csv View File

@@ -0,0 +1,10 @@
1
+id,number,type,productType,size,color,quantity,isHandled,variantId,importance
2
+1,R00001,new,T-shirt,Large,White,2000,FALSE,,1
3
+1,R00001,new,T-shirt,Medium,White,20,FALSE,,1
4
+1,R00001,new,T-shirt,Large,Navy Blue,20,FALSE,,1
5
+2,R00002,new,Dress,Large,Forest Green,20,FALSE,,2
6
+2,R00002,new,Dress,Medium,Soft Gray,20,FALSE,,2
7
+2,R00002,new,Dress,Large,Forest Green,20,FALSE,,2
8
+2,R00002,new,Dress,Medium,Soft Gray,30,FALSE,,2
9
+3,R00003,reorder,Dress,Large,Forest Green,30,FALSE,1,3
10
+3,R00003,reorder,Dress,Medium,Forest Green,30,FALSE,2,3

+ 23
- 4
WebContent/src/db/init-data.js View File

@@ -25,6 +25,26 @@ function initProducts() {
25 25
   return init(objects)
26 26
 }
27 27
 
28
+function initRecommendation() {
29
+  let objects = [
30
+    'recommendations'
31
+  ];
32
+
33
+  let combinedObjParam = {
34
+    objectsToCombine: 'recommendations',
35
+    attrsToCombine: [
36
+      'variantId',
37
+      'size',
38
+      'color',
39
+      'quantity'
40
+    ],
41
+    identifier: 'id',
42
+    combinedKey: 'variants'
43
+  };
44
+
45
+  return init(objects, combinedObjParam);
46
+}
47
+
28 48
 function initPurchasing() {
29 49
   let objects = [
30 50
     'purchase-orders',
@@ -119,11 +139,9 @@ function initObjects(name, nameCamelCase, combinedObjParam) {
119 139
             value = parseFloat(value).toFixed(2);
120 140
           } else if (!isNaN(value) && value) {
121 141
             value = parseInt(value);
122
-          }
123
-          
124
-          if (value === 'false') {
142
+          } else if (value.toLowerCase() === 'false') {
125 143
             value = false;
126
-          } else if (value === 'true') {
144
+          } else if (value.toLowerCase() === 'true') {
127 145
             value = true;
128 146
           }
129 147
           object[key] = value
@@ -198,6 +216,7 @@ function resetIds(objects) {
198 216
 export {
199 217
   initUsers,
200 218
   initProducts,
219
+  initRecommendation,
201 220
   initPurchasing,
202 221
   initSuppliers,
203 222
   initWarehouses,

+ 7
- 2
WebContent/src/db/purchasing.js View File

@@ -5,7 +5,7 @@ import {
5 5
   log,
6 6
 } from '../utils/utils'
7 7
 
8
-import { PURCHASE_ORDERS } from './setting'
8
+import { PURCHASE_ORDERS, RECOMMENDATION } from './setting'
9 9
 
10 10
 function addPurchaseOrder(purchaseOrder) {
11 11
   let purchaseOrders = JSON.parse(localStorage.getItem(PURCHASE_ORDERS));
@@ -23,7 +23,12 @@ function updatePurchaseOrder(purchaseOrder) {
23 23
   localStorage.setItem(PURCHASE_ORDERS, JSON.stringify(purchaseOrders));
24 24
 }
25 25
 
26
+function udpateRecommendation(recommendations) {
27
+  localStorage.setItem(RECOMMENDATION, recommendations);
28
+}
29
+
26 30
 export {
27 31
   addPurchaseOrder,
28
-  updatePurchaseOrder
32
+  updatePurchaseOrder,
33
+  udpateRecommendation
29 34
 }

+ 1
- 0
WebContent/src/db/setting.js View File

@@ -4,6 +4,7 @@
4 4
 export const PRODUCTS = 'products';
5 5
 export const VARIANTS = 'variants';
6 6
 export const PURCHASE_ORDERS = 'purchaseOrders';
7
+export const RECOMMENDATION = 'recommendation';
7 8
 export const WAREHOUSE = 'warehouses';
8 9
 export const CELL_VARAINT_JOINS = 'cellVariantJoins';
9 10
 

+ 5
- 1
WebContent/src/store/modules/inventory.js View File

@@ -57,7 +57,11 @@ const getters = {
57 57
       return product.type === type
58 58
     });
59 59
   },
60
-  
60
+
61
+  getVariantsByTypeColorSize: (state, getters) => (type, color, size) => {
62
+    return state.variants.filter(variant => variant.color === color && variant.size === size && getters.getProductById(variant.productId).type === type)
63
+  },
64
+
61 65
   sizesOfType: state => (type) => {
62 66
     let filteredVariants = state.variants.filter(v => {
63 67
       let product = state.products.find(p => p.id === v.productId);

+ 21
- 0
WebContent/src/store/modules/menu/purchasing.js View File

@@ -34,6 +34,16 @@ export default {
34 34
         link: 'purchasing/purchase-orders.vue'
35 35
       }
36 36
     },
37
+    {
38
+      title: 'Recommendations',
39
+      path: '/recommendations',
40
+      router: true,
41
+      isMenu: true,
42
+      component: lazyLoading('purchasing/recommendations'),
43
+      meta: {
44
+        link: 'purchasing/recommendations.vue'
45
+      }
46
+    },
37 47
     {
38 48
       title: 'Reorder',
39 49
       path: '/reorder',
@@ -49,6 +59,11 @@ export default {
49 59
       isMenu: false,
50 60
       component: lazyLoading('purchasing/purchase-order-details')
51 61
     },
62
+    {
63
+      path: '/purchaseOrders/create/purchaseOrderDetails/:number',
64
+      isMenu: false,
65
+      component: lazyLoading('purchasing/purchase-order-details')
66
+    },
52 67
     {
53 68
       path: '/purchaseOrders/view/:id',
54 69
       canReuse: false,
@@ -60,6 +75,12 @@ export default {
60 75
       canReuse: false,
61 76
       isMenu: false,
62 77
       component: lazyLoading('purchasing/purchase-order-details')
78
+    },
79
+    {
80
+      path: '/recommendations/:type',
81
+      canReuse: false,
82
+      isMenu: false,
83
+      component: lazyLoading('purchasing/recommendations')
63 84
     }
64 85
   ]
65 86
 }

+ 84
- 6
WebContent/src/store/modules/purchasing.js View File

@@ -13,23 +13,28 @@ import {
13 13
   log} from '../../utils/utils';
14 14
 
15 15
 import {
16
-  initPurchasing
16
+  initPurchasing,
17
+  initRecommendation
17 18
 } from '../../db/init-data'
18 19
 
19 20
 import {
20 21
   addPurchaseOrder,
21
-  updatePurchaseOrder
22
+  updatePurchaseOrder,
23
+  udpateRecommendation
22 24
 } from '../../db/purchasing'
23 25
 
24 26
 import * as s from '../../utils/setting'
25 27
 
26 28
 const state = {
27
-  purchaseOrders: []
29
+  purchaseOrders: [],
30
+  recommendations: []
28 31
 };
29 32
 
30 33
 const getters = {
31 34
   purchaseOrders: state => state.purchaseOrders,
32 35
 
36
+  recommendations: state => state.recommendations,
37
+
33 38
   getOrderByNumber: (state, getters) => (orderNumber) => state.purchaseOrders.find((order) => order.orderNumber === orderNumber),
34 39
 
35 40
   receivedQuantityPercentage: (state, getters) => (orderNumber) => {
@@ -178,7 +183,7 @@ const getters = {
178 183
       data.title = s.PURCHASING_DASHBOARD_TITLES[i];
179 184
       data.unit = s.PURCHASING_DASHBOARD_UNITS[i];
180 185
       data.isIncreased = percentage[i] > 0;
181
-      data.percentage = percentage[i];
186
+      data.percentage = percentage[i] === Infinity? 0 : percentage[i];
182 187
       data.value = values[i];
183 188
       info.push(data);
184 189
     }
@@ -186,7 +191,54 @@ const getters = {
186 191
     log(info);
187 192
     return info
188 193
   },
189
-  
194
+
195
+  getDashboardAction: (state, getters) => {
196
+    let actions = []
197
+      , action;
198
+
199
+    action = {
200
+      id: 'to-purchase',
201
+      title: 'To Purchase',
202
+      number: getters.toPurchaseNew.length,
203
+      unit: 'Recommendations',
204
+      href: '/recommendations'
205
+    };
206
+
207
+    actions.push(action);
208
+
209
+    action = {
210
+      id: 'to-reorder',
211
+      title: 'To Reorder',
212
+      number: getters.toReorder.length,
213
+      unit: 'Recommendations',
214
+      href: '/reorder'
215
+    };
216
+
217
+    actions.push(action);
218
+
219
+    action = {
220
+      id: 'to-adjust',
221
+      title: 'To Adjust',
222
+      number: getters.toAdjust.length,
223
+      unit: 'Purcahse Orders',
224
+      href: '/purchaseOrder/adjustments'
225
+    };
226
+
227
+    actions.push(action);
228
+
229
+    return actions;
230
+  },
231
+
232
+
233
+  // Recommendations
234
+  toPurchaseNew: state => state.recommendations.filter(r => r.type === 'new'),
235
+
236
+  toReorder: state => state.recommendations.filter(r => r.type === 'reorder'),
237
+
238
+  toAdjust: state => state.purchaseOrders.filter(p => p.adjustments.length > 0),
239
+
240
+  getRecommendationByNumber: state => (number) => state.recommendations.find(r => r.number === number),
241
+
190 242
   // Get statuses
191 243
   getInboundStatuses() {
192 244
     return [s.ALL, s.STATUS_PURCHASED, s.STATUS_RECEIVED, s.STATUS_CHECKED, s.STATUS_STORED]
@@ -225,7 +277,15 @@ const mutations = {
225 277
       }
226 278
       return p;
227 279
     });
228
-    
280
+
281
+    let recommendationObj = initRecommendation();
282
+    state.recommendations = recommendationObj.recommendations.map(r => {
283
+      r.quantity = r.variants.map(v => v.quantity).reduce((sum, quantity) => { return sum + quantity });
284
+      return r;
285
+    });
286
+
287
+    state.purchaseOrders.sort((a, b) => a.created - b.created);
288
+
229 289
     log('purchaseOrders', state.purchaseOrders);
230 290
   },
231 291
 
@@ -275,6 +335,8 @@ const mutations = {
275 335
     purchaseOrder.receives = [];
276 336
     purchaseOrder.adjustments = [];
277 337
     purchaseOrder.receivedPercentage = 0;
338
+    purchaseOrder.checkedPercentage = 0;
339
+    purchaseOrder.storedPercentage = 0;
278 340
     purchaseOrder.toChecks = [];
279 341
     purchaseOrder.checkedItems = [];
280 342
     purchaseOrder.toStores = [];
@@ -477,6 +539,16 @@ const mutations = {
477 539
   [types.ADJUST_PURCHASE] (state, {purchaseOrder, adjustment}) {
478 540
     purchaseOrder.adjustments.push(adjustment);
479 541
     updatePurchaseOrder(purchaseOrder);
542
+  },
543
+
544
+  [types.HANDLE_RECOMMENDATION] (state, recommendation) {
545
+    state.recommendations.map(r => {
546
+      if (r.id === recommendation.id) {
547
+        r = recommendation;
548
+      }
549
+      return r;
550
+    });
551
+    udpateRecommendation(state.recommendations);
480 552
   }
481 553
 };
482 554
 
@@ -568,6 +640,12 @@ const actions = {
568 640
       commit(types.UPDATE_STOCK, { increaseAttr, decreaseAttr, items, itemAttr });
569 641
       // Todo: send message to purchasing crew to create new order
570 642
     }
643
+  },
644
+
645
+  handleRecommendation ({commit, getters}, number) {
646
+    log('handle recommendation');
647
+    let recommendation = getters.getRecommendationByNumber(number);
648
+    commit(types.HANDLE_RECOMMENDATION, recommendation);
571 649
   }
572 650
 
573 651
 };

+ 9
- 5
WebContent/src/store/modules/supplier.js View File

@@ -45,11 +45,11 @@ const getters = {
45 45
     return supplier.brand;
46 46
   },
47 47
   
48
-  getSupplierByName: (state, getters) => (name) => {
49
-    return state.suppliers.find(s => s.name === name);
50
-  },
51
-  
52 48
   // Supplier Contacts
49
+  getSupplierContactsBySupplierId: (state) => (supplierId) => {
50
+    return state.supplierContacts.filter(s => s.supplierId === supplierId);
51
+  },
52
+
53 53
   getSupplierContactsByName: (state, getters) => (name) => {
54 54
     let supplierId = getters.getSupplierByName(name).id;
55 55
     return state.supplierContacts.filter((sc) => sc.supplierId === supplierId).map(sc => sc.email);
@@ -57,10 +57,14 @@ const getters = {
57 57
   
58 58
   getSupplierContactByEmail: (state) => (email) => {
59 59
     return state.supplierContacts.find(sc => sc.email === email);
60
+  },
61
+
62
+  getSupplierByVariant: (state, getters) => (variant) => {
63
+    return state.suppliers.find(s => s.id === getters.getProductById(variant.productId).supplierId);
60 64
   }
61
-  
62 65
 };
63 66
 
67
+
64 68
 const mutations = {
65 69
   [types.INIT_SUPPLIER] (state) {
66 70
     let supplierObj = initSuppliers();

+ 2
- 0
WebContent/src/store/mutation-types.js View File

@@ -50,5 +50,7 @@ export const ADJUST_PURCHASE = 'ADJUST_PURCHASE'
50 50
 
51 51
 export const SAVE_PURCHASE = 'SAVE_PURCHASE'
52 52
 
53
+export const HANDLE_RECOMMENDATION = 'HANDLE_RECOMMENDATION'
54
+
53 55
 // Warehouse
54 56
 export const ALLOCATE_ITEMS = 'ALLOCATE_ITEMS'

+ 69
- 0
WebContent/src/views/components/dashboard-action-card.vue View File

@@ -0,0 +1,69 @@
1
+<template>
2
+  <v-card :id="action.id" :data="action.href">
3
+    <v-card-text>
4
+      <v-card-row>
5
+        <p class="card-title">{{ action.title }}</p>
6
+      </v-card-row>
7
+
8
+      <v-card-row>
9
+        <p class="card-number">{{ action.number }}</p>
10
+      </v-card-row>
11
+
12
+      <v-card-row>
13
+        <p class="card-unit">{{ action.unit }}</p>
14
+      </v-card-row>
15
+
16
+    </v-card-text>
17
+  </v-card>
18
+</template>
19
+
20
+<script>
21
+
22
+  export default {
23
+    name: 'ActionCard',
24
+
25
+    props: ['action'],
26
+
27
+    methods: {
28
+      cardClicked (href) {
29
+        this.$router.replace(href);
30
+      }
31
+    },
32
+
33
+    data() {
34
+      return {
35
+
36
+      }
37
+    },
38
+
39
+    mounted () {
40
+      let $card = document.getElementById(this.action.id);
41
+      $card.addEventListener('click', () => this.cardClicked($card.getAttribute('data')));
42
+    }
43
+  }
44
+</script>
45
+
46
+<style scoped>
47
+  .card:hover {
48
+    background: #f4f4f5;
49
+    cursor: pointer;
50
+  }
51
+
52
+  .card-title {
53
+    font-size: 15px;
54
+  }
55
+
56
+  .card-number {
57
+    font-size: 30px;
58
+    font-weight: 500;
59
+    color: #F34336;
60
+  }
61
+
62
+  .card-unit {
63
+    font-size: 15px;
64
+  }
65
+
66
+  p {
67
+    margin-bottom: 0;
68
+  }
69
+</style>

WebContent/src/views/components/datacard.vue → WebContent/src/views/components/dashboard-info-card.vue View File

@@ -9,7 +9,7 @@
9 9
       </v-card-row>
10 10
 
11 11
       <v-card-row>
12
-        <div>
12
+        <div style="width: 100%">
13 13
           <v-icon class="mr-1 percentage-icon increase"
14 14
                   v-if="info.isIncreased">keyboard_arrow_up</v-icon>
15 15
           <v-icon class="mr-1 percentage-icon decrease"
@@ -48,7 +48,7 @@
48 48
     font-weight: 500;
49 49
   }
50 50
 
51
-  .card-percentage {
51
+  .action {
52 52
     float: left;
53 53
   }
54 54
 

+ 202
- 0
WebContent/src/views/purchasing/components/demand-forecast.vue View File

@@ -0,0 +1,202 @@
1
+<template>
2
+  <v-card>
3
+    <v-toolbar class="demand-forecast-chart-toolbar">
4
+      <v-toolbar-title class="chart-toolbar-title">Demand Forecast</v-toolbar-title>
5
+
6
+      <v-select
7
+          class="chart-toolbar-select"
8
+          v-bind:items="productTypes"
9
+          v-model="type"
10
+          label="Select product type"
11
+          dark
12
+      />
13
+
14
+      <v-select
15
+          class="chart-toolbar-select"
16
+          v-bind:items="colors"
17
+          v-model="color"
18
+          label="Select color"
19
+          dark
20
+      />
21
+
22
+      <v-select
23
+          class="chart-toolbar-select"
24
+          v-bind:items="sizes"
25
+          v-model="size"
26
+          label="Select size"
27
+          dark
28
+      />
29
+
30
+    </v-toolbar>
31
+    <v-container fluid class="demand-forecast-container">
32
+
33
+      <chart :type="'line'" :data="predictionData" :options="predictionOptions"></chart>
34
+
35
+    </v-container>
36
+  </v-card>
37
+</template>
38
+
39
+<script>
40
+  import { mapGetters, mapActions } from 'vuex'
41
+  import Chart from '../../components/chartjs.vue'
42
+
43
+  export default {
44
+    name: 'DemandForecast',
45
+
46
+    props: ['productType'],
47
+
48
+    components: {
49
+      Chart
50
+    },
51
+
52
+    computed: {
53
+      ...mapGetters([
54
+        'getPredictions',
55
+        'getLabels',
56
+        'getSales',
57
+        'getVariantIdsByTypeColorSize',
58
+        'productTypes',
59
+        'sizesOfType',
60
+        'colorsOfType'
61
+      ]),
62
+    },
63
+
64
+    watch: {
65
+      type () {
66
+        if (this.type) {
67
+          this.sizes = this.sizesOfType(this.type);
68
+          this.colors = this.colorsOfType(this.type);
69
+          this.size = this.sizes[0];
70
+          this.color = this.colors[0];
71
+        }
72
+        this.setPredictions();
73
+      },
74
+
75
+      size () {
76
+        this.setPredictions();
77
+      },
78
+
79
+      color () {
80
+        this.setPredictions();
81
+      },
82
+
83
+      demandChartData () {
84
+        this.predictionData = {
85
+          labels: this.labels,
86
+        };
87
+        this.predictionData.datasets = this.predictionSeries.map((e, i) => {
88
+          return {
89
+            data: this.demandChartData[i],
90
+            label: this.predictionSeries[i],
91
+            borderColor: this.transparentBgColor[i].replace(/1\)$/, '.5)'),
92
+            pointBackgroundColor: this.transparentBgColor[i],
93
+            backgroundColor: this.transparentBgColor[i].replace(/1\)$/, '.5)')
94
+          }
95
+        });
96
+      },
97
+    },
98
+
99
+    methods: {
100
+
101
+      setPredictions() {
102
+        // Todo: find all related ids instead of one
103
+        if (this.type && this.size && this.color) {
104
+          let variantId = this.getVariantIdsByTypeColorSize(this.type, this.size, this.color);
105
+          this.sales = this.getSales(variantId);
106
+          this.predictions = this.getPredictions(this.sales);
107
+          this.labels = this.getLabels(variantId);
108
+
109
+          let year = 2017;
110
+          let month = 1;
111
+          for (let i = 0; i < 12; i++) {
112
+            this.labels.push([year, month].join('-'));
113
+            if (month === 12) {
114
+              month = 1;
115
+              year++;
116
+            } else {
117
+              month++
118
+            }
119
+          }
120
+          this.predictionOptions.scales.yAxes[0].ticks.min = Math.min.apply(null, this.sales.concat(this.predictions.slice(this.sales.length)));
121
+          this.demandChartData = [this.sales, this.predictions];
122
+        }
123
+      }
124
+    },
125
+
126
+    mounted() {
127
+      this.type = this.productType;
128
+    },
129
+
130
+    data () {
131
+      return {
132
+        // demand forecast attributes
133
+        sales: [],
134
+        labels: [],
135
+        predictions: [],
136
+        predictionSeries: ['actual', 'prediction'],
137
+        demandChartData: [],
138
+        predictionOptions: {
139
+          scales: {
140
+            xAxes: [{}],
141
+            yAxes: [{
142
+              ticks: {}
143
+            }]
144
+          }
145
+        },
146
+        predictionData: {},
147
+
148
+        type: '',
149
+        size: '',
150
+        color: '',
151
+        sizes: [],
152
+        colors: [],
153
+
154
+        transparentBgColor: [
155
+          'rgba(31, 200, 219, 1)',
156
+          'rgba(151, 205, 118, 1)'
157
+        ],
158
+      }
159
+    },
160
+
161
+    created() {
162
+      this.$store.dispatch('initInventory');
163
+      this.$store.dispatch('initSales');
164
+      this.$store.dispatch('initPurchasing');
165
+    },
166
+  }
167
+</script>
168
+
169
+<style scoped>
170
+  .row {
171
+    margin-bottom: 1.5rem;
172
+  }
173
+
174
+  .chart-container {
175
+    padding-top: 10px;
176
+    padding-bottom: 5px;
177
+  }
178
+
179
+  .chart-toolbar {
180
+    height: 50px;
181
+  }
182
+
183
+  .chart-toolbar-title {
184
+    font-size: 17px;
185
+  }
186
+
187
+  .chart-toolbar-select {
188
+    margin: 16px 0 0 16px;
189
+  }
190
+
191
+  .demand-forecast-container {
192
+    padding: 20px 20px;
193
+  }
194
+
195
+  .demand-forecast-chart-container {
196
+    padding-left: 20px;
197
+  }
198
+
199
+  .demand-forecast-chart-toolbar {
200
+    height: 80px;
201
+  }
202
+</style>

+ 2
- 2
WebContent/src/views/purchasing/components/purchase-orders-content.vue View File

@@ -133,7 +133,7 @@
133 133
         }
134 134
       },
135 135
 
136
-      setPersonnel () {
136
+      setData () {
137 137
         let urlsParts = window.location.href.split('/');
138 138
         this.page = urlsParts.pop();
139 139
         // Two conditions: inbound and purchasing
@@ -164,7 +164,7 @@
164 164
     },
165 165
 
166 166
     mounted() {
167
-      this.setPersonnel();
167
+      this.setData();
168 168
       // Add eventListener to tabs
169 169
       let $tabs = document.getElementsByClassName('tab');
170 170
       Array.from($tabs).forEach($tab => {

+ 3
- 2
WebContent/src/views/purchasing/components/warehouse-map-dialog.vue View File

@@ -221,7 +221,7 @@
221 221
         Array.from(variantIds).forEach(variantId => {
222 222
           cellVariants = this.cellVariantJoins.filter(cv => cv.variantId === variantId);
223 223
           this.allocations = this.allocations.concat(this.getVariantAllocations(cellVariants));
224
-        })
224
+        });
225 225
         this.allocations.sort((a, b) => a.location - b.location);
226 226
       }
227 227
     },
@@ -243,7 +243,8 @@
243 243
         'getCellVariantsByShelfLayerName',
244 244
         'getCellVariantsByShelfLayerCellName',
245 245
         'getCellVariantsByShelfName',
246
-        'getVariantsOfCellVariants'
246
+        'getVariantsOfCellVariants',
247
+        'getVariantAllocations'
247 248
       ]),
248 249
 
249 250
       layerNames() {

+ 11
- 4
WebContent/src/views/purchasing/dashboard.vue View File

@@ -3,8 +3,12 @@
3 3
     <breadcrumbs :items="breadcrumbs" :isSlim="true"></breadcrumbs>
4 4
     <v-container class="dashboard-container" fluid>
5 5
       <v-row>
6
-        <v-col v-for="card in getDashboardInfo" :key="card.id" xs4>
7
-          <card :info="card"></card>
6
+        <v-col v-for="(action, index) in getDashboardAction" :key="index" xs2>
7
+          <action-card :action="action"></action-card>
8
+        </v-col>
9
+
10
+        <v-col v-for="info in getDashboardInfo" :key="info.id" xs2>
11
+          <info-card :info="info"></info-card>
8 12
         </v-col>
9 13
       </v-row>
10 14
 
@@ -138,7 +142,8 @@
138 142
 
139 143
 <script>
140 144
   import Breadcrumbs from '../components/breadcrumbs.vue'
141
-  import Card from '../components/datacard.vue'
145
+  import ActionCard from '../components/dashboard-action-card.vue'
146
+  import InfoCard from '../components/dashboard-info-card.vue'
142 147
   import Chart from '../components/chartjs.vue'
143 148
 
144 149
   import { mapGetters, mapActions } from 'vuex'
@@ -149,13 +154,15 @@
149 154
 
150 155
     components: {
151 156
       Breadcrumbs,
152
-      Card,
157
+      ActionCard,
158
+      InfoCard,
153 159
       Chart
154 160
     },
155 161
 
156 162
     computed: {
157 163
       ...mapGetters([
158 164
         'getDashboardInfo',
165
+        'getDashboardAction',
159 166
         'getPredictions',
160 167
         'getLabels',
161 168
         'getSales',

+ 51
- 4
WebContent/src/views/purchasing/purchase-order-details.vue View File

@@ -216,6 +216,10 @@
216 216
         </v-card>
217 217
       </v-col>
218 218
     </v-row>
219
+
220
+    <demand-forecast
221
+        v-if="recommendationNumber"
222
+        :productType="recommendation.productType"></demand-forecast>
219 223
   </div>
220 224
 
221 225
 
@@ -233,6 +237,7 @@
233 237
   import AdjustmentDialog from './components/adjustment-dialog.vue'
234 238
   import AdjustedItems from './components/adjusted-items.vue'
235 239
   import PurchaseSummary from './components/purchase-order-summary.vue'
240
+  import DemandForecast from './components/demand-forecast.vue'
236 241
   import { mapGetters, mapActions } from 'vuex'
237 242
   import * as s from '../../utils/setting'
238 243
 
@@ -253,7 +258,8 @@
253 258
       StoredItems,
254 259
       AdjustmentDialog,
255 260
       AdjustedItems,
256
-      PurchaseSummary
261
+      PurchaseSummary,
262
+      DemandForecast
257 263
     },
258 264
 
259 265
     watch: {
@@ -280,11 +286,16 @@
280 286
         'getVariantById',
281 287
         'getSupplierById',
282 288
         'fulfillVariants',
283
-        'fulfillNestedVariants'
289
+        'fulfillNestedVariants',
290
+        'getRecommendationByNumber',
291
+        'getVariantsByTypeColorSize',
292
+        'getSupplierByVariant',
293
+        'getSupplierContactsByName',
294
+        'getSupplierContactsBySupplierId'
284 295
       ]),
285 296
       supplierContacts() {
286 297
         if (this.order.supplier) {
287
-          return this.$store.getters.getSupplierContactsByName(this.order.supplier);
298
+          return this.getSupplierContactsByName(this.order.supplier);
288 299
         } else {
289 300
           return []
290 301
         }
@@ -293,7 +304,8 @@
293 304
 
294 305
     methods: {
295 306
       ...mapActions([
296
-        'createPurchase'
307
+        'createPurchase',
308
+        'handleRecommendation'
297 309
       ]),
298 310
 
299 311
       validateOrder() {
@@ -307,6 +319,11 @@
307 319
             order: this.order,
308 320
             items: this.orderedItems
309 321
           });
322
+
323
+          if (this.recommendationNumber) {
324
+            this.handleRecommendation(this.recommendationNumber);
325
+          }
326
+
310 327
           this.$router.replace('/purchaseOrders');
311 328
         })
312 329
         .catch(err => {
@@ -372,6 +389,11 @@
372 389
           childText = 'Create New Purchase Order';
373 390
         }
374 391
 
392
+        this.recommendationNumber = this.$route.params.number;
393
+        if (this.recommendationNumber) {
394
+          this.setRecommendation(this.recommendationNumber);
395
+        }
396
+
375 397
         this.breadcrumbs = [
376 398
           {
377 399
             text: parentText,
@@ -382,6 +404,27 @@
382 404
         ];
383 405
       },
384 406
 
407
+      setRecommendation (number) {
408
+        this.recommendation = this.getRecommendationByNumber(number);
409
+        Array.from(this.recommendation.variants).forEach(variant => {
410
+          let variants = this.getVariantsByTypeColorSize(this.recommendation.productType, variant.color, variant.size);
411
+          variants = variants.map(v => {
412
+            v.quantity = variant.quantity;
413
+            return v;
414
+          });
415
+          this.orderedItems = this.orderedItems.concat(variants);
416
+
417
+        });
418
+
419
+        let supplier = this.getSupplierByVariant(this.orderedItems[0]);
420
+        this.order.supplierId = supplier.id;
421
+        this.order.supplier = supplier.name;
422
+        this.order.warehouse = this.warehouseLocations[0];
423
+        let supplierContacts = this.getSupplierContactsBySupplierId(supplier.id);
424
+        this.order.contact = supplierContacts[0].email;
425
+        this.order.supplierContactId = supplierContacts[0].id;
426
+      },
427
+
385 428
       setSameHeight () {
386 429
         // Set height of items container equal to summary container
387 430
         let $container = document.getElementById('items-container');
@@ -492,6 +535,10 @@
492 535
         isToCheck: false,
493 536
         isToStore: false,
494 537
         orderId: '',
538
+
539
+        // recommendations
540
+        recommendationNumber: '',
541
+        recommendation: {}
495 542
       }
496 543
     },
497 544
 

+ 163
- 0
WebContent/src/views/purchasing/recommendations.vue View File

@@ -0,0 +1,163 @@
1
+<template>
2
+  <div>
3
+    <breadcrumbs :items="breadcrumbs" :isSlim="true"></breadcrumbs>
4
+    <v-card>
5
+      <v-data-table
6
+          v-bind:headers="headers"
7
+          v-model="tableItems"
8
+          v-bind:search="searchContent"
9
+          rows-per-page="10"
10
+          :rows-per-page-items="rowsPerPageItems"
11
+      >
12
+
13
+        <template slot="items" scope="props">
14
+          <td class="recommend-td hidden"><input v-model="props.item.number"></td>
15
+          <td>{{ props.item.number }}</td>
16
+          <td>
17
+            <v-chip
18
+                label class="table-chip white--text"
19
+                v-bind:class="{
20
+                  green: props.item.type === 'new',
21
+                  primary: props.item.type === 'reorder'}"
22
+            >
23
+              {{ props.item.type }}
24
+            </v-chip>
25
+          </td>
26
+          <td>{{ props.item.productType }}</td>
27
+          <td>{{ props.item.quantity }}</td>
28
+          <td>{{ props.item.importance }}</td>
29
+          <td>{{ props.item.isHandled }}</td>
30
+        </template>
31
+      </v-data-table>
32
+    </v-card>
33
+  </div>
34
+</template>
35
+
36
+<script>
37
+  import Breadcrumbs from '../components/breadcrumbs.vue'
38
+  import { mapGetters, mapActions } from 'vuex'
39
+
40
+  export default {
41
+    name: 'Recommendations',
42
+
43
+    components: {
44
+      Breadcrumbs
45
+    },
46
+
47
+    computed: {
48
+      ...mapGetters([
49
+        'toPurchaseNew',
50
+        'toReorder',
51
+        'recommendations'
52
+      ])
53
+    },
54
+
55
+    methods: {
56
+      rowOnClick: function (number) {
57
+        console.log('row clicked', number);
58
+        let routeTo = '/purchaseOrders/create/purchaseOrderDetails/' + number;
59
+        this.$router.replace(routeTo);
60
+      },
61
+
62
+      setData () {
63
+        let urlsParts = window.location.href.split('/');
64
+        let page = urlsParts.pop();
65
+        switch (page) {
66
+          case 'new':
67
+            this.tableItems = this.toPurchaseNew;
68
+            break;
69
+          case 'reorder':
70
+            this.tableItems = this.toReorder;
71
+            break;
72
+          default:
73
+            this.tableItems = this.recommendations;
74
+        }
75
+      },
76
+
77
+      addRowEvent () {
78
+        let $recommendTds = document.getElementsByClassName('recommend-td');
79
+        Array.from($recommendTds).forEach(($td) => {
80
+          $td.parentNode.addEventListener('click', () => this.rowOnClick($td.firstChild.value))
81
+        });
82
+      }
83
+    },
84
+
85
+    created() {
86
+      this.$store.dispatch('initPurchasing');
87
+      this.setData();
88
+    },
89
+
90
+    mounted() {
91
+      this.addRowEvent();
92
+    },
93
+
94
+    beforeDestroy() {
95
+      let $recommendTd = document.getElementsByClassName('recommend-td');
96
+      Array.from($recommendTd).forEach(($td) => {
97
+        $td.parentNode.removeEventListener('click', () => this.rowOnClick($td.firstChild.value))
98
+      });
99
+    },
100
+
101
+    data () {
102
+      return {
103
+        breadcrumbs: [
104
+          { text: 'Recommendations' }
105
+        ],
106
+
107
+        searchContent: '',
108
+        tableItems: [],
109
+        rowsPerPageItems: [10, 15, 25, { text: "All", value: -1 }],
110
+        headers: [{
111
+          text: 'Recommendation #',
112
+          left: true,
113
+          value: 'number',
114
+        }, {
115
+          text: 'Type',
116
+          value: 'type',
117
+          left: true
118
+        }, {
119
+          text: 'productType',
120
+          value: 'productType',
121
+          left: true
122
+        }, {
123
+          text: 'Quantity',
124
+          value: 'quantity',
125
+          left: true
126
+        }, {
127
+          text: 'Importance',
128
+          value: 'importance',
129
+          left: true
130
+        }, {
131
+          text: 'Is Handled',
132
+          value: 'isHandled',
133
+          left: true
134
+        }]
135
+      }
136
+    }
137
+  }
138
+</script>
139
+
140
+<style scoped>
141
+  .table-chip {
142
+    margin: 0;
143
+    padding: 0 8px;
144
+    height: 25px;
145
+    font-size: 13px;
146
+  }
147
+
148
+  .datatable {
149
+    margin-bottom: 200px;
150
+  }
151
+
152
+  .tools {
153
+    margin-top: -10px;
154
+  }
155
+
156
+  table tbody tr:hover td {
157
+    cursor: pointer !important;
158
+  }
159
+
160
+  .table__overflow {
161
+    margin-bottom: 12rem !important;
162
+  }
163
+</style>

+ 2
- 1
WebContent/src/views/warehouse/index.vue View File

@@ -130,7 +130,8 @@
130 130
         Array.from(variantIds).forEach(variantId => {
131 131
           cellVariants = this.cellVariantJoins.filter(cv => cv.variantId === variantId);
132 132
           this.allocations = this.allocations.concat(this.getVariantAllocations(cellVariants));
133
-        })
133
+        });
134
+        this.allocations.sort((a, b) => a.location - b.location);
134 135
       }
135 136
     },
136 137
 

Loading…
Cancel
Save