Parcourir la source

[IMP] purchase features

Gogs il y a 7 ans
Parent
commit
a41d5cb8e9

+ 13 - 3
controllers/main.py

@@ -147,6 +147,8 @@ class Purchases(http.Controller):
             'variantCount': product.product_variant_count,
             'quantity': 1,
             'price': product.standard_price,
+            'minimumPrice': product.minimum_price,
+            'maximumPrice': product.maximum_price,
             'variants': [{
                 'id': variant.id,
                 'name': variant.name,
@@ -155,7 +157,9 @@ class Purchases(http.Controller):
                 'imageMedium': variant.image_medium,
                 'standardPrice': variant.standard_price,
                 'quantity': 1,
-                'price': variant.standard_price
+                'price': variant.standard_price,
+                'minimumPrice': product.minimum_price,
+                'maximumPrice': product.maximum_price
             } for variant in product.product_variant_ids if variant.active]
         } for product in product_obj.search(domain)]
         
@@ -277,15 +281,21 @@ class Purchases(http.Controller):
             'displayName': product.display_name,
             'ean13': product.ean13,
             'imageMedium': product.image_medium,
-            'listPrice': product.list_price,
+            'standardPrice': product.standard_price,
             'variantCount': product.product_variant_count,
+            'price': product.list_price,
+            'minimumPrice': product.minimum_price,
+            'maximumPrice': product.maximum_price,
             'variants': [{
                 'id': variant.id,
                 'name': variant.name,
                 'displayName': variant.display_name,
                 'ean13': variant.ean13,
                 'imageMedium': variant.image_medium,
-                'standardPrice': variant.standard_price
+                'standardPrice': product.standard_price,
+                'price': variant.standard_price,
+                'minimumPrice': product.minimum_price,
+                'maximumPrice': product.maximum_price
             } for variant in product.product_variant_ids if variant.active]
         }
 

+ 2 - 1
src/assets/_variables.sass

@@ -4,4 +4,5 @@ $app-light-color: #f5f5f5
 $app-bg-color: #fff
 $app-border-color: #d3d3d3
 $app-title-color: #d3d3d3
-$app-separator-color: #9e9e9e
+$app-separator-color: #9e9e9e
+$app-error-color: #ef9a9a

+ 17 - 6
src/components/common/Card.vue

@@ -2,8 +2,8 @@
     .card(@click='onClick' :class="{ 'selected-card': isSelected }")
         h2.card-title {{ title }}
         img.card-image(:src="'data:image/png;base64,' + (image || 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAACXZwQWcAAABAAAAAQADq8/hgAAAEWklEQVRYw9WX6XKjRhCAef8HiySQvGt5vfZuEselOUAcEpe4GdI9MAgQOjb5k3SVyzY1801PX9OtNf9StP80QJR5miRpXtb/AFCnvmMySgmhlJn2Mal+BSBSj1NCGeNSGAMOd0/iQYCI95TAXnm+FCr/I2ZYPwJILEJhPaGm7flBFIW+Z5sUvwEivguovG7pMR0cV2e+BbYArF3cBqQclKfEvryvSB2KaHa6BYhgDSP7ZN7gmUNQCf86wCdgcBaKq04/cTzAuwbA/czKb8VdZYMSI8IAEOJ+XjTiFkF4SDjOARIIHLiBK+4E/xHOIdEloMSAAwZx7hEOBKIquwA4lFPbR/3uEhzCqSUmgBiwrGgeIlQm5b0zO0CN3yKw34QgQC4JKZqrGAFC0MpWvuwJ3V6hWD3BI5wchoDaBAumzYQgmsrd7ewZx5bosHIAAAtQp4+nXUuA+2yXy9Xyi4OsIorjauBLZQWtd0Gqrt3EvCXQlb4BMZYfsPP7cr0gvS4FaNw6Qus0ovtez8DZcYyHt8Wmk9XWdF+Mjf570Ke4q46UgAgUCtX55mKl/wSbsD83hrEE0VGJ1RrEWHz2aaXuIAEe7b3SNG/601oSzL/W20/T2r2uDNACARvjWelZQTTaCiCg2vSR1bzrsFgSQMk8SbPi8FWX+0GFbX2OXMarDoAmOGfo+wpXt7cwj4Hv+1n+rSMYW3HOfS4TAgHZIDIVYG38wNzchyB+kj4ZUwB4npw6ABokmgA2qz9kfbIkoWDLzQSQ0tbw2gA20kA/nmyqCHG8nmqQd2prbSKQZAIwnk5B5PSE/EWfACCUZGFSgHQKeE6DsCcExfc5wKEDRLMaJHBwTwA/zFzhOLBBPGODoCfEyYUb0XVBB1AGHXvho/SVDsSjF15QrtMG1xlpsDbCrCewj7UxAWAJSjsAlJOuHI0AX9Mi8IMgsJnMC2MMOJA2f7RhXI8AG/2LVxZZVlQWmKElnAFiT5nMH62L67Mb3lTmbIzVK3Uc9r6GvJAEyMa6d0KXP1oXliqbRPPzN0NvBcrBAmSpr37wlrB8GeRS6zkJECZVNRKeuLfty1C+wc/zp7TD9jVQN7DUDq2vkUEzfAymIl9uZ5iL1B0U1Rw7surmc4SE/sUBE3KaDB8Wd1QS7hJQga4Kayow2aAsXiV0L458HE/jx9UbPi33CIf+ITwDSnxM/IcIcAGIrHzaH+BX8Ky4awdq41nBZYsjG4/kEQLjg9Q5A9A1jJ7u3CJEa1OzmuvSKgubwPA24IT7WT7fJ5YmEtwbASWO2AkP94871WpPOCc8vmYHaORhv5lf75VrV3bD+9nZIrUJamhXN9v9kMlu3wonYVlGe9msU1/cGTgKpx0YmO2fsrKq66rMk8Bh7dd99sDIk+xxxsE5icqhqfsLflkz1pkbukSCBzI5bqG0EGrPGvfK2FeGDseRi1I5eVFuB8WvDp51FvsH13Fcz4+y6n86Oz8kfwPMD02INEiadQAAAABJRU5ErkJggg==')")
-        .card-description(v-if='!!description')
-            span {{ description }}
+        .card-details(v-if='details.length > 0')
+            span(v-for='detail in details') {{ computeDetail(detail) }}
 </template>
 
 <script>
@@ -17,16 +17,27 @@
                 type: String,
                 default: 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAACXZwQWcAAABAAAAAQADq8/hgAAAEWklEQVRYw9WX6XKjRhCAef8HiySQvGt5vfZuEselOUAcEpe4GdI9MAgQOjb5k3SVyzY1801PX9OtNf9StP80QJR5miRpXtb/AFCnvmMySgmhlJn2Mal+BSBSj1NCGeNSGAMOd0/iQYCI95TAXnm+FCr/I2ZYPwJILEJhPaGm7flBFIW+Z5sUvwEivguovG7pMR0cV2e+BbYArF3cBqQclKfEvryvSB2KaHa6BYhgDSP7ZN7gmUNQCf86wCdgcBaKq04/cTzAuwbA/czKb8VdZYMSI8IAEOJ+XjTiFkF4SDjOARIIHLiBK+4E/xHOIdEloMSAAwZx7hEOBKIquwA4lFPbR/3uEhzCqSUmgBiwrGgeIlQm5b0zO0CN3yKw34QgQC4JKZqrGAFC0MpWvuwJ3V6hWD3BI5wchoDaBAumzYQgmsrd7ewZx5bosHIAAAtQp4+nXUuA+2yXy9Xyi4OsIorjauBLZQWtd0Gqrt3EvCXQlb4BMZYfsPP7cr0gvS4FaNw6Qus0ovtez8DZcYyHt8Wmk9XWdF+Mjf570Ke4q46UgAgUCtX55mKl/wSbsD83hrEE0VGJ1RrEWHz2aaXuIAEe7b3SNG/601oSzL/W20/T2r2uDNACARvjWelZQTTaCiCg2vSR1bzrsFgSQMk8SbPi8FWX+0GFbX2OXMarDoAmOGfo+wpXt7cwj4Hv+1n+rSMYW3HOfS4TAgHZIDIVYG38wNzchyB+kj4ZUwB4npw6ABokmgA2qz9kfbIkoWDLzQSQ0tbw2gA20kA/nmyqCHG8nmqQd2prbSKQZAIwnk5B5PSE/EWfACCUZGFSgHQKeE6DsCcExfc5wKEDRLMaJHBwTwA/zFzhOLBBPGODoCfEyYUb0XVBB1AGHXvho/SVDsSjF15QrtMG1xlpsDbCrCewj7UxAWAJSjsAlJOuHI0AX9Mi8IMgsJnMC2MMOJA2f7RhXI8AG/2LVxZZVlQWmKElnAFiT5nMH62L67Mb3lTmbIzVK3Uc9r6GvJAEyMa6d0KXP1oXliqbRPPzN0NvBcrBAmSpr37wlrB8GeRS6zkJECZVNRKeuLfty1C+wc/zp7TD9jVQN7DUDq2vkUEzfAymIl9uZ5iL1B0U1Rw7surmc4SE/sUBE3KaDB8Wd1QS7hJQga4Kayow2aAsXiV0L458HE/jx9UbPi33CIf+ITwDSnxM/IcIcAGIrHzaH+BX8Ky4awdq41nBZYsjG4/kEQLjg9Q5A9A1jJ7u3CJEa1OzmuvSKgubwPA24IT7WT7fJ5YmEtwbASWO2AkP94871WpPOCc8vmYHaORhv5lf75VrV3bD+9nZIrUJamhXN9v9kMlu3wonYVlGe9msU1/cGTgKpx0YmO2fsrKq66rMk8Bh7dd99sDIk+xxxsE5icqhqfsLflkz1pkbukSCBzI5bqG0EGrPGvfK2FeGDseRi1I5eVFuB8WvDp51FvsH13Fcz4+y6n86Oz8kfwPMD02INEiadQAAAABJRU5ErkJggg=='
             },
-            description: {
-                type: String,
-                default: ''
+            details: {
+                type: Array,
+                default: []
             },
             isSelected: {
                 type: Boolean,
                 default: true
+            },
+            options: {
+                type: Object,
+                default: {}
             }
         },
         methods: {
+            computeDetail(detail) {
+                if (detail.format === 'currency') {
+                    return this.$options.filters.currency(detail.value, {...this.options})
+                }
+
+                return detail.value
+            },
             onClick() {
                 this.$emit('onClick')
             }
@@ -67,7 +78,7 @@
             left: 50%
             margin-right: -50%
             transform: translate(-50%, -50%)
-        .card-description
+        .card-details
             width: 100%
             height: 30px
             padding-top: 5px

+ 76 - 7
src/components/common/CardGrid.vue

@@ -4,7 +4,7 @@
             spinner(type='wave')
         .card-grid(v-else)
             add-card(v-if='canAdd' @onClickAdd='onClickAdd')
-            card(v-for='item in items' :key='item.id' :title='item.name' :image='item.imageMedium' :isSelected='item.id === selectedId' :description='getDescription(item)' @onClick='onClickCard(item)')
+            card(v-for='item in items' :key='item.id' :title='item.name' :image='item.imageMedium' :isSelected='item.id === selectedId' :details='computeDetails(item)' :options='defaultOptions.currency' @onClick='onClickCard(item)')
 </template>
 
 <script>
@@ -22,13 +22,17 @@
                 type: Boolean,
                 default: false
             },
-            description: {
-                type: String,
-                default: ''
+            details: {
+                type: Array,
+                default: []
             },
             loading: {
                 type: Boolean,
                 default: false
+            },
+            options: {
+                type: Object,
+                default: {}
             }
         },
         components: {
@@ -36,9 +40,65 @@
             Card,
             Spinner
         },
+        watch: {
+            options(value) {
+                this.computeOptions(value)
+            }
+        },
         methods: {
-            getDescription(item) {
-                return (!!this.description && item[this.description]) || ''
+            computeDetails(item) {
+                if (!this.details) {
+                    return []
+                }
+
+                if (this.details.length === 0) {
+                    return []
+                }
+
+                let results = []
+                let computableDetails = this.details.map(item => item.split(/:/))
+
+                for (let detail of computableDetails) {
+                    for (let field in item) {
+                        if (field === detail[0]) {
+                            results.push({
+                                value: item[field],
+                                format: (() => {
+                                    if (!detail[1] || detail[1] === 's') {
+                                        return 'string'
+                                    }
+                                    
+                                    if (detail[1] === 'c') {
+                                        return 'currency'
+                                    }
+
+                                    if (detail[1] === 'd') {
+                                        return 'date'
+                                    }
+
+                                    return 'string'
+                                })()
+                            })
+
+                            break
+                        }
+                    }
+                }
+
+                return results
+            },
+            computeOptions(value) {
+                if (!value) {
+                    return
+                }
+
+                for(let key in value) {
+                    if(!this.defaultOptions.currency[key]) {
+                        continue
+                    }
+
+                    this.defaultOptions.currency[key] = value[key]
+                }
             },
             onClickAdd() {
                 this.$emit('onAdd')
@@ -50,7 +110,16 @@
         },
         data() {
             return {
-                selectedId: -1
+                selectedId: -1,
+                defaultOptions: {
+                    currency: {
+                        symbol: '$',
+                        position: 'before',
+                        thousandsSeparator: '.',
+                        decimalPlaces: 2,
+                        decimalSeparator: ',' 
+                    },
+                } 
             }
         }
     }

+ 5 - 2
src/components/common/Cart.vue

@@ -4,7 +4,7 @@
             h2.currency-cart-total {{ total | currency(...defaultOptions.currency) }}
         .cart-items-wrapper
             transition-group(name='list' tag='ul' class='cart-items')
-                cart-item(v-for='(item, index) in items' :key='index' :index='index' :item='item' @onChange='onItemChanged' @onClickIncrement='onIncrementQty' @onClickDecrement='onDecrementQty' @onClickDelete='onDeleteItem' :options='defaultOptions.currency')
+                cart-item(v-for='(item, index) in items' :key='index' :index='index' :item='item' @onChange='onItemChanged' @onClickIncrement='onIncrementQty' @onClickDecrement='onDecrementQty' @onClickMoney='onChangePrice' @onClickDelete='onDeleteItem' :options='defaultOptions.currency')
 </template>
 
 <script>
@@ -43,7 +43,7 @@
                 let sum = 0
 
                 for (let item of this.items) {
-                    sum = sum + ((item.price || 1) * (item.quantity || 1))
+                    sum = sum + ((item.price || 0) * (item.quantity || 0))
                 }
 
                 this.total = sum
@@ -59,6 +59,9 @@
             onDecrementQty(item) {
                 this.$emit('onDecrementQty', item)
             },
+            onChangePrice(item) {
+                this.$emit('onChangePrice', item)
+            },
             onDeleteItem(item) {
                 this.$emit('onDeleteItem', item)
             }

+ 14 - 3
src/components/common/CartItem.vue

@@ -1,7 +1,7 @@
 <template lang="pug">
-    li.cart-item
+    li.cart-item(:class="{'cart-item-invalid': !isValid()}")
         h3.item-name {{ item.displayName }}
-        input.item-quantity(type='number' min='1' :value='item.quantity')
+        input.item-quantity(type='number' min='1' :value='item.quantity' readonly)
         span.item-x x
         span.item-price {{ item.price | currency(...options) }}
         span.item-equals =
@@ -10,6 +10,7 @@
             .cart-item-options
                 .cart-item-option(class='fa fa-plus' @click='onClickIncrement')
                 .cart-item-option(class='fa fa-minus' @click='onClickDecrement')
+                .cart-item-option(class="fa fa-money" @click='onClickMoney')
                 .cart-item-option(class='fa fa-trash' @click='onClickDelete')
 </template>
 
@@ -55,8 +56,14 @@
             onClickDecrement() {
                 this.$emit('onClickDecrement', this.item)
             },
+            onClickMoney() {
+                this.$emit('onClickMoney', this.item)
+            },
             onClickDelete() {
                 this.$emit('onClickDelete', this.item)
+            },
+            isValid() {
+                return this.item.price > 0
             }
         }
     }
@@ -71,6 +78,8 @@
         border-bottom: 1px solid $app-border-color
         box-sizing: border-box
         position: relative
+        &.cart-item-invalid
+            border-bottom: 2px solid $app-error-color
         &:nth-child(1)
             border-top: 1px solid $app-border-color
         &:hover
@@ -126,7 +135,7 @@
             display: flex
             justify-content: center
             .cart-item-options
-                width: 90px
+                width: 100px
                 height: 20px
                 border: 1px solid #d3d3d3
                 border-bottom: none
@@ -146,6 +155,8 @@
                             color: #2196f3
                         &.fa-minus:hover
                             color: #ffc107
+                        &.fa-money:hover
+                            color: #4caf50
                         &.fa-trash:hover
                             color: #f44336
 </style>

+ 39 - 1
src/components/common/Searcher.vue

@@ -52,10 +52,18 @@
                 type: Array,
                 default: [],
                 required: true
+            },
+            mode: {
+                type: String,
+                default: 'fuzzy'
             }
         },
         watch: {
             items(values) {
+                if (this.mode !== 'fuzzy') {
+                    return
+                }
+
                 this.fuse.setCollection(values)
             },
             search(value) {
@@ -78,7 +86,33 @@
                 })
             },
             performSearch(value) {
-                this.results = this.fuse.search(value)
+                if(!value) {
+                    this.results = []
+                    return
+                }
+
+                if (this.mode === 'fuzzy') {
+                    this.results = this.fuse.search(value)
+                } else {
+                    this.results = []
+
+                    for (let item of this.items) {
+                        for (let field in item) {
+                            if (typeof item[field] !== 'string') {
+                                continue
+                            }
+
+                            if (this.keys.length !== 0 && this.keys.indexOf(field) === -1) {
+                                continue
+                            }
+
+                            if (item[field].toLowerCase().indexOf(value.toLowerCase()) !== -1) {
+                                this.results.push(item)
+                                break
+                            }
+                        }
+                    }
+                }
             }
         },
         data() {
@@ -89,6 +123,10 @@
             }
         },
         mounted() {
+            if (this.mode !== 'fuzzy') {
+                return
+            }
+            
             this.initFuse()
         }
     }

+ 9 - 0
src/components/filters/absolute.js

@@ -0,0 +1,9 @@
+/**
+ * 
+ * @param {*} value 
+ */
+const absolute = (value = 0) => {
+    return Math.abs(value)
+}
+
+export default absolute

+ 183 - 0
src/components/modals/PriceModal.vue

@@ -0,0 +1,183 @@
+<template lang="pug">
+    modal(name='product-price' transition='nice-modal-fade' @before-close='beforeClose' :classes="['v--modal', 'product-price']")
+        form
+            .form-item
+                label.form-label Precio unitario
+                input.form-input(:value='(!item || item.price) | currency(...options)' readonly)
+            .form-item
+                label.form-label Precio mínimo
+                input.form-input(:value='(!item || item.minimumPrice) | currency(...options)' readonly)
+            .form-item
+                label.form-label Precio máximo
+                input.form-input(:value='(!item || item.maximumPrice) | currency(...options)' readonly)
+            hr
+            .form-item
+                label.form-label Precio aplicado
+                input.form-input(v-model='formattedAmmount' :class="{'price-input-invalid': !isValid()}" autofocus)
+            .form-item
+                label.form-label Diferencia
+                input.form-input(:value='residual | absolute | currency(...options)' readonly)
+            .form-actions
+                button.form-action(@click='onAccept' :disabled='isValid() === false') Aceptar
+                button.form-action(@click='onCancel') Cancelar
+</template> 
+
+<script>
+    export default {
+        props: {
+            item: {
+                type: Object,
+                default: {
+                    price: 0,
+                    minimumPrice: 0,
+                    maximumPrice: 0
+                }
+            },
+            options: {
+                type: Object,
+                default: {
+                    symbol: '$',
+                    position: 'before',
+                    thousandsSeparator: '.',
+                    decimalPlaces: 2,
+                    decimalSeparator: ','
+                }
+            },
+            show: {
+                type: Boolean,
+                default: false
+            }
+        },
+        computed: {
+            formattedAmmount: {
+                get() {
+                    let formatted = this.$options.filters.currency(this.ammount, {...this.options})
+                    return this.ammount !== 0 ? formatted : formatted.replace(/\d/, '')
+                },
+                set(value) {
+                    value = value.replace(/[\.|,](\d{0,2}$)/, '?$1').split(/\?/)
+                    value[0] = value[0].replace(/[^0-9]/g, '')
+                    value = Number.parseFloat(value.join('.')) || 0
+
+                    this.ammount = value
+                    this.computeResisual()
+                }
+            }
+        },
+        watch: {
+            show(value) {
+                if (value) {
+                    this.$modal.show('product-price')
+                } else {
+                    this.ammount = 0
+                    this.residual = 0
+
+                    this.$modal.hide('product-price')
+                }
+            }
+        },
+        methods: {
+            beforeClose(e) {
+                if (this.show) {
+                    e.stop()
+                }
+            },
+            onAccept() {
+                this.$emit('onAccept', this.ammount)
+            },
+            onCancel() {
+                this.$emit('onCancel')
+            },
+            computeResisual() {
+                if (this.ammount !== 0) {
+                    this.residual = this.item.price - this.ammount
+                } else {
+                    this.residual = 0
+                }
+            },
+            isValid() {
+                if (!this.item) {
+                    return false
+                }
+
+                if (this.ammount === 0) {
+                    return false
+                }
+
+                if (this.item.minimumPrice === 0 && this.item.maximumPrice === 0) {
+                    return true
+                }
+
+                if (this.ammount >= this.item.minimumPrice && this.ammount <= this.item.maximumPrice) {
+                    return true
+                }
+
+                return false
+            }
+        },
+        data() {
+            return {
+                ammount: 0,
+                residual: 0
+            }
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+    .product-price
+        width: 600px
+        height: 340px !important
+        form
+            width: 100%
+            height: 290px
+            padding: 15px
+            .form-item
+                width: 100%
+                height: 45px
+                margin-bottom: 10px
+                &:nth-child(1)
+                    .form-input
+                        border: none
+                &:nth-child(2), &:nth-child(3), &:nth-child(6) 
+                    height: 35px
+                    margin-bottom: 5px
+                    .form-label
+                        width: 30%
+                        height: 35px
+                        font-size: 10pt
+                        color: $app-dark-color
+                    .form-input
+                        width: 70%
+                        height: 35px
+                        font-size: 18pt
+                        text-align: right
+                        border: none
+                .form-label
+                    width: 30%
+                    height: 45px
+                    font-size: 14pt
+                .form-input
+                    width: 70%
+                    height: 45px
+                    font-size: 28pt
+                    text-align: right
+                    border-radius: 0
+                    &.form-input-invalid
+                        border-color: $app-error-color
+                        box-shadow: 1px 1px 2px $app-error-color, -1px -1px 2px $app-error-color
+                        &:focus
+                            outline: none
+            .form-actions
+                float: right
+                .form-action
+                    width: 160px
+                    height: 40px
+                    border: none
+                    box-shadow: none
+                    border-radius: 0
+                    margin-right: 5px
+                    background: $app-main-color
+                    color: $app-bg-color
+</style>

+ 92 - 0
src/components/modals/VariantModal.vue

@@ -0,0 +1,92 @@
+<template lang="pug">
+    modal(name='variant-selector' transition='nice-modal-fade' @closed='onClose' height='500' :classes="['v--modal', 'variant-selector']")
+        searcher(:items='items' :keys="['name', 'displayName', 'ean13', 'defaultCode']" mode='normal' @onSearch='filterItems')
+        .product-variants
+            .product-variant(v-for='item in getItems()' :key='item.id' @click='onSelect(item)')
+                img.variant-image(:src="'data:image/png;base64,' + (item.imageMedium || 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAACXZwQWcAAABAAAAAQADq8/hgAAAEWklEQVRYw9WX6XKjRhCAef8HiySQvGt5vfZuEselOUAcEpe4GdI9MAgQOjb5k3SVyzY1801PX9OtNf9StP80QJR5miRpXtb/AFCnvmMySgmhlJn2Mal+BSBSj1NCGeNSGAMOd0/iQYCI95TAXnm+FCr/I2ZYPwJILEJhPaGm7flBFIW+Z5sUvwEivguovG7pMR0cV2e+BbYArF3cBqQclKfEvryvSB2KaHa6BYhgDSP7ZN7gmUNQCf86wCdgcBaKq04/cTzAuwbA/czKb8VdZYMSI8IAEOJ+XjTiFkF4SDjOARIIHLiBK+4E/xHOIdEloMSAAwZx7hEOBKIquwA4lFPbR/3uEhzCqSUmgBiwrGgeIlQm5b0zO0CN3yKw34QgQC4JKZqrGAFC0MpWvuwJ3V6hWD3BI5wchoDaBAumzYQgmsrd7ewZx5bosHIAAAtQp4+nXUuA+2yXy9Xyi4OsIorjauBLZQWtd0Gqrt3EvCXQlb4BMZYfsPP7cr0gvS4FaNw6Qus0ovtez8DZcYyHt8Wmk9XWdF+Mjf570Ke4q46UgAgUCtX55mKl/wSbsD83hrEE0VGJ1RrEWHz2aaXuIAEe7b3SNG/601oSzL/W20/T2r2uDNACARvjWelZQTTaCiCg2vSR1bzrsFgSQMk8SbPi8FWX+0GFbX2OXMarDoAmOGfo+wpXt7cwj4Hv+1n+rSMYW3HOfS4TAgHZIDIVYG38wNzchyB+kj4ZUwB4npw6ABokmgA2qz9kfbIkoWDLzQSQ0tbw2gA20kA/nmyqCHG8nmqQd2prbSKQZAIwnk5B5PSE/EWfACCUZGFSgHQKeE6DsCcExfc5wKEDRLMaJHBwTwA/zFzhOLBBPGODoCfEyYUb0XVBB1AGHXvho/SVDsSjF15QrtMG1xlpsDbCrCewj7UxAWAJSjsAlJOuHI0AX9Mi8IMgsJnMC2MMOJA2f7RhXI8AG/2LVxZZVlQWmKElnAFiT5nMH62L67Mb3lTmbIzVK3Uc9r6GvJAEyMa6d0KXP1oXliqbRPPzN0NvBcrBAmSpr37wlrB8GeRS6zkJECZVNRKeuLfty1C+wc/zp7TD9jVQN7DUDq2vkUEzfAymIl9uZ5iL1B0U1Rw7surmc4SE/sUBE3KaDB8Wd1QS7hJQga4Kayow2aAsXiV0L458HE/jx9UbPi33CIf+ITwDSnxM/IcIcAGIrHzaH+BX8Ky4awdq41nBZYsjG4/kEQLjg9Q5A9A1jJ7u3CJEa1OzmuvSKgubwPA24IT7WT7fJ5YmEtwbASWO2AkP94871WpPOCc8vmYHaORhv5lf75VrV3bD+9nZIrUJamhXN9v9kMlu3wonYVlGe9msU1/cGTgKpx0YmO2fsrKq66rMk8Bh7dd99sDIk+xxxsE5icqhqfsLflkz1pkbukSCBzI5bqG0EGrPGvfK2FeGDseRi1I5eVFuB8WvDp51FvsH13Fcz4+y6n86Oz8kfwPMD02INEiadQAAAABJRU5ErkJggg==')")
+                .variant-details
+                    h2.variant-name {{ item.displayName }}
+</template>
+
+<script>
+    import { mapGetters, mapActions } from 'vuex'
+    import { Searcher } from '../common'
+
+    export default {
+        components: {
+            Searcher
+        },
+        props: {
+            items: {
+                type: Array,
+                default: []
+            },
+            show: {
+                type: Boolean,
+                default: false
+            }
+        },
+        watch: {
+            show(value) {
+                if (value) {
+                    this.$modal.show('variant-selector')
+                } else {
+                    this.filteredItems = []
+
+                    this.$modal.hide('variant-selector')
+                }
+            }
+        },
+        methods: {
+            getItems() {
+                return this.filteredItems.length === 0 ? this.items : this.filteredItems
+            },
+            filterItems(values) {
+                this.filteredItems = values
+            },
+            onSelect(item) {
+                this.$emit('onSelect', item)
+            },
+            onClose() {
+                this.$emit('onClose')
+            }
+        },
+        data() {
+            return {
+                filteredItems: []
+            }
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+    .variant-selector
+        width: 600px
+        padding: 15px !important
+        .product-variants
+            width: 100%
+            heigth: calc(100% - 20px)
+            overflow-y: auto
+            .product-variant
+                width: calc(100% - 20px)
+                heigth: 84px
+                margin: 5px 10px
+                display: flex
+                &:hover
+                    cursor: pointer
+                    .variant-details
+                        transition-duration: 500ms
+                        border-bottom: 2px solid $app-main-color
+                .variant-image
+                    width: 80px
+                    heigth: 80px
+                    margin: 0
+                    border: none
+                .variant-details
+                    flex-grow: 1
+                    border-bottom: 1px solid $app-border-color
+                    .variant-name
+                        font-size: 10pt
+                        margin-left: 10px
+</style>

+ 13 - 4
src/components/steps/Product.vue

@@ -2,33 +2,42 @@
     .purchase-step
         .products-selector
             searcher(:items='products' :keys="['name', 'displayName', 'ean13']" @onSearch='filterProducts')
-            card-grid(:items='visibleProducts' :loading='loadingProducts' @onSelect='selectProduct')
-        cart(:items='cartItems' @onIncrementQty='addToCart' @onDecrementQty='decreaseFromCart' @onDeleteItem='removeFromCart' @onTotalComputed='updateCartTotal' :options='selectedCurrency')
+            card-grid(:items='visibleProducts' :loading='loadingProducts' :details="['price:c']" @onSelect='selectProduct')
+            variant-modal(:items='productWithVariant && productWithVariant.variants' :show='!!productWithVariant' @onSelect='selectProduct' @onClose='selectProduct')
+            price-modal(:item='itemPriced' :options='selectedCurrency' :show='!!itemPriced' @onAccept='changePrice' @onCancel='changePrice')
+        cart(:items='cartItems' @onIncrementQty='addToCart' @onChangePrice='changePrice' @onDecrementQty='decreaseFromCart' @onDeleteItem='removeFromCart' @onTotalComputed='updateCartTotal' :options='selectedCurrency')
 </template>
 
 <script>
     import { mapGetters, mapActions } from 'vuex'
     
     import { Searcher, CardGrid, Cart } from '../common'
+    import VariantModal from '../modals/VariantModal'
+    import PriceModal from '../modals/PriceModal'
 
     export default {
         components: {
             Searcher,
             CardGrid,
-            Cart
+            Cart,
+            VariantModal,
+            PriceModal
         },
         computed: mapGetters([
             'products',
             'visibleProducts',
             'loadingProducts',
             'cartItems',
-            'selectedCurrency'
+            'selectedCurrency',
+            'productWithVariant',
+            'itemPriced'
         ]),
         methods: mapActions([
             'filterProducts',
             'selectProduct',
             'addToCart',
             'decreaseFromCart',
+            'changePrice',
             'removeFromCart',
             'updateCartTotal'
         ])

+ 2 - 0
src/index.js

@@ -4,8 +4,10 @@ import VueModal from 'vue-js-modal'
 import store from '@/store'
 
 import currency from '@/components/filters/currency'
+import absolute from '@/components/filters/absolute'
 
 Vue.filter('currency', currency)
+Vue.filter('absolute', absolute)
 
 Vue.use(VueModal)
 

+ 48 - 1
src/store/modules/cart.js

@@ -1,6 +1,7 @@
 const state = {
     cartItems: [],
-    cartTotal: 0
+    cartTotal: 0,
+    itemPriced: null
 }
 
 const getters = {
@@ -17,6 +18,13 @@ const getters = {
      */
     cartTotal(state) {
         return state.cartTotal
+    },
+    /**
+     * 
+     * @param {*} state 
+     */
+    itemPriced(state) {
+        return state.itemPriced
     }
 }
 
@@ -52,6 +60,10 @@ const mutations = {
     pullFromCart(state, payload) {
         let foundIndex = state.cartItems.findIndex(item => item.id === payload.item.id)
 
+        if(state.cartItems[foundIndex].quantity === 1) {
+            payload.mode = 'full'
+        }
+
         if (payload.mode === 'partial') {
             state.cartItems[foundIndex].quantity = state.cartItems[foundIndex].quantity - 1
         } else {
@@ -65,6 +77,23 @@ const mutations = {
      */
     setCartTotal(state, payload) {
         state.cartTotal = payload
+    },
+    /**
+     * 
+     * @param {*} state 
+     * @param {*} payload 
+     */
+    setItemPriced(state, payload) {
+        state.itemPriced = payload
+    },
+    /**
+     * 
+     * @param {*} state 
+     * @param {*} payload 
+     */
+    applyPrice(state, payload) {
+        let foundProduct = state.cartItems.find(item => item.id === state.itemPriced.id)
+        foundProduct.price = payload
     }
 }
 
@@ -99,6 +128,24 @@ const actions = {
             mode: 'partial'
         })
     },
+    /**
+     * 
+     * @param {*} param0 
+     * @param {*} payload 
+     */
+    changePrice({ commit }, payload) {
+        if (typeof payload === 'number') {
+           commit('applyPrice', payload)
+        }
+
+        if (typeof payload === 'object') {
+            commit('setItemPriced', payload)
+            return
+        }
+
+        commit('setItemPriced', null)
+
+    },
     /**
      * 
      * @param {*} param0 

+ 12 - 2
src/store/modules/product.js

@@ -93,13 +93,23 @@ const actions = {
      * @param {*} param0 
      * @param {*} payload 
      */
-    selectProduct({ commit }, payload) {
+    selectProduct({ commit, dispatch }, payload) {
+        if(!payload) {
+            commit('setProductWithVariant', null)
+            return
+        }
+
         if (payload.variantCount > 1) {
             commit('setProductWithVariant', payload)
             return
         }
 
-        commit('pushToCart', payload.variants[0])
+        if (!payload.variantCount) {
+            dispatch('addToCart', payload)
+            commit('setProductWithVariant', null)
+        } else {
+            dispatch('addToCart', payload.variants[0])
+        }
     },
     /**
      *