@viur/shop-components 0.15.0 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +2 -3
- package/package.json +1 -1
- package/src/ShopSummary.vue +83 -12
- package/src/components/AddressForm.vue +19 -2
- package/src/composables/cart.js +55 -72
- package/src/translations/de.js +1 -0
- package/src/translations/en.js +1 -0
- package/src/translations/fr.js +1 -0
|
@@ -20,11 +20,9 @@ jobs:
|
|
|
20
20
|
uses: actions/checkout@v4
|
|
21
21
|
- uses: actions/setup-node@v4
|
|
22
22
|
with:
|
|
23
|
-
node-version:
|
|
23
|
+
node-version: 24
|
|
24
24
|
registry-url: https://registry.npmjs.org/
|
|
25
25
|
|
|
26
|
-
- name: Ensure npm supports trusted publishing
|
|
27
|
-
run: npm install -g npm@latest
|
|
28
26
|
- run: npm ci
|
|
29
27
|
|
|
30
28
|
- name: Determine npm tag
|
|
@@ -42,6 +40,7 @@ jobs:
|
|
|
42
40
|
else
|
|
43
41
|
echo "tag=latest" >> $GITHUB_OUTPUT
|
|
44
42
|
fi
|
|
43
|
+
|
|
45
44
|
- name: Publish to npm (Trusted Publishing)
|
|
46
45
|
run: npm publish --access public --tag ${{ steps.npm_tag.outputs.tag }}
|
|
47
46
|
|
package/package.json
CHANGED
package/src/ShopSummary.vue
CHANGED
|
@@ -3,16 +3,34 @@
|
|
|
3
3
|
<h2 class="viur-shop-cart-sidebar-headline headline" v-html="$t('viur.shop.summary_headline')"></h2>
|
|
4
4
|
<div class="viur-shop-cart-sidebar-summary">
|
|
5
5
|
<div class="viur-shop-cart-sidebar-summary-item" v-for="item in state.items">
|
|
6
|
-
<template v-if="(!shopStore.state.
|
|
7
|
-
<div class="viur-shop-cart-sidebar-summary-item-
|
|
8
|
-
|
|
6
|
+
<template v-if="(!shopStore.state.showN1odes && item.skel_type === 'leaf') || shopStore.state.showNodes">
|
|
7
|
+
<div class="viur-shop-cart-sidebar-summary-item-row">
|
|
8
|
+
<div class="viur-shop-cart-sidebar-summary-item-amount" v-if="item.skel_type === 'leaf'">
|
|
9
|
+
{{ item.quantity }} ×
|
|
10
|
+
</div>
|
|
11
|
+
<div class="viur-shop-cart-sidebar-summary-item-name" v-html="item.skel_type === 'node' ? item.name : item.shop_name"></div>
|
|
12
|
+
<div class="viur-shop-cart-sidebar-summary-item-price" v-if="getArticleDiscounts(item).length && item.price?.recommended > item.price?.current">
|
|
13
|
+
<sl-badge v-for="discount in getArticleDiscounts(item)" variant="danger" pill>
|
|
14
|
+
<template v-if="discount.discount_type === 'percentage'">-{{ discount.percentage }}%</template>
|
|
15
|
+
<template v-else>
|
|
16
|
+
-<sl-format-number lang="de" type="currency" currency="EUR" :value="discount.absolute"></sl-format-number>
|
|
17
|
+
</template>
|
|
18
|
+
</sl-badge>
|
|
19
|
+
<sl-format-number lang="de" type="currency" currency="EUR"
|
|
20
|
+
:value="(item.total ? item.total : item.price?.current) * (item.quantity || 1)">
|
|
21
|
+
</sl-format-number>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="viur-shop-cart-sidebar-summary-item-price" v-else>
|
|
24
|
+
<sl-format-number lang="de" type="currency" currency="EUR"
|
|
25
|
+
:value="(item.total ? item.total : item.price?.current) * (item.quantity || 1)">
|
|
26
|
+
</sl-format-number>
|
|
27
|
+
</div>
|
|
9
28
|
</div>
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
:value="item.total ? item.total : item.price?.current">
|
|
29
|
+
<span class="viur-shop-cart-sidebar-summary-item-price--uvp" v-if="getArticleDiscounts(item).length && item.price?.recommended > item.price?.current">
|
|
30
|
+
UVP: <sl-format-number lang="de" type="currency" currency="EUR"
|
|
31
|
+
:value="item.price.recommended * (item.quantity || 1)">
|
|
14
32
|
</sl-format-number>
|
|
15
|
-
</
|
|
33
|
+
</span>
|
|
16
34
|
</template>
|
|
17
35
|
</div>
|
|
18
36
|
</div>
|
|
@@ -27,8 +45,8 @@
|
|
|
27
45
|
<sl-format-number lang="de" type="currency" currency="EUR" :value="state.shippingTotal">
|
|
28
46
|
</sl-format-number>
|
|
29
47
|
</div>
|
|
30
|
-
<div class="viur-shop-cart-sidebar-info" v-if="shopStore.state.cartRoot.discount">
|
|
31
|
-
<span
|
|
48
|
+
<div class="viur-shop-cart-sidebar-info" v-if="shopStore.state.cartRoot.discount && isBasketDiscount(shopStore.state.cartRoot.discount.dest.key)">
|
|
49
|
+
<span>{{ shopStore.state.cartRoot.discount.dest.name }}</span>
|
|
32
50
|
<sl-format-number lang="de" type="currency" currency="EUR" :value="state.discount">
|
|
33
51
|
</sl-format-number>
|
|
34
52
|
</div>
|
|
@@ -76,7 +94,7 @@ import LoadingHandler from "./components/LoadingHandler.vue"
|
|
|
76
94
|
import DiscountInput from './components/DiscountInput.vue';
|
|
77
95
|
|
|
78
96
|
const shopStore = useViurShopStore();
|
|
79
|
-
const { fetchCart, addItem, state: cartState } = useCart();
|
|
97
|
+
const { fetchCart, fetchCartRoot, addItem, state: cartState } = useCart();
|
|
80
98
|
|
|
81
99
|
const props = defineProps({
|
|
82
100
|
showFeatures: {type: Boolean, default: true},
|
|
@@ -133,12 +151,40 @@ const state = reactive({
|
|
|
133
151
|
loading:false
|
|
134
152
|
})
|
|
135
153
|
|
|
154
|
+
function isBasketDiscount(discountKey) {
|
|
155
|
+
const item = state.items.find(i => i.price?.cart_discounts?.length)
|
|
156
|
+
if (!item) return false
|
|
157
|
+
const discount = item.price.cart_discounts.find(d => d.key === discountKey)
|
|
158
|
+
if (!discount) return false
|
|
159
|
+
return !(discount.condition || []).some(c => c.dest?.application_domain === 'article')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getArticleDiscounts(item) {
|
|
163
|
+
if (!item.price?.cart_discounts) return []
|
|
164
|
+
return item.price.cart_discounts.filter(discount =>
|
|
165
|
+
(discount.condition || []).some(c => c.dest?.application_domain === 'article')
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function calcDiscountValue(discount, item) {
|
|
170
|
+
const quantity = item.quantity || 1
|
|
171
|
+
const recommended = item.price?.recommended || 0
|
|
172
|
+
const current = item.price?.current || 0
|
|
173
|
+
if (recommended > current) {
|
|
174
|
+
return (recommended - current) * quantity
|
|
175
|
+
}
|
|
176
|
+
if (discount.discount_type === 'percentage') {
|
|
177
|
+
return recommended * quantity * discount.percentage / 100
|
|
178
|
+
}
|
|
179
|
+
return (discount.absolute || 0) * quantity
|
|
180
|
+
}
|
|
181
|
+
|
|
136
182
|
onBeforeMount(() => {
|
|
137
183
|
state.loading=true
|
|
138
184
|
if (!shopStore.state.cartList.length) {
|
|
139
185
|
fetchCart().then(()=>state.loading=false).catch(()=>state.loading=false)
|
|
140
186
|
}else{
|
|
141
|
-
state.loading=false
|
|
187
|
+
fetchCartRoot().then(()=>state.loading=false).catch(()=>state.loading=false)
|
|
142
188
|
}
|
|
143
189
|
})
|
|
144
190
|
|
|
@@ -210,13 +256,38 @@ function calc_percent(val){
|
|
|
210
256
|
}
|
|
211
257
|
|
|
212
258
|
.viur-shop-cart-sidebar-summary-item {
|
|
259
|
+
display: flex;
|
|
260
|
+
flex-direction: column;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.viur-shop-cart-sidebar-summary-item-row {
|
|
213
264
|
display: flex;
|
|
214
265
|
flex-direction: row;
|
|
215
266
|
flex-wrap: nowrap;
|
|
216
267
|
gap: var(--sl-spacing-medium);
|
|
268
|
+
align-items: baseline;
|
|
217
269
|
}
|
|
218
270
|
|
|
219
271
|
.viur-shop-cart-sidebar-summary-item-name {
|
|
220
272
|
margin-right: auto;
|
|
273
|
+
min-width: 0;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
text-overflow: ellipsis;
|
|
276
|
+
white-space: nowrap;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.viur-shop-cart-sidebar-summary-item-price {
|
|
280
|
+
flex-shrink: 0;
|
|
281
|
+
display: flex;
|
|
282
|
+
gap: var(--sl-spacing-x-small);
|
|
283
|
+
align-items: baseline;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.viur-shop-cart-sidebar-summary-item-price--uvp {
|
|
287
|
+
color: var(--sl-color-neutral-400);
|
|
288
|
+
font-size: var(--sl-font-size-small);
|
|
289
|
+
white-space: nowrap;
|
|
290
|
+
text-align: right;
|
|
291
|
+
display: block;
|
|
221
292
|
}
|
|
222
293
|
</style>
|
|
@@ -13,11 +13,22 @@
|
|
|
13
13
|
error-style="decent"
|
|
14
14
|
>
|
|
15
15
|
</ViForm>
|
|
16
|
+
<ShopAlert
|
|
17
|
+
v-if="state.showCountryChangeHint"
|
|
18
|
+
class="country-change-hint"
|
|
19
|
+
variant="warning"
|
|
20
|
+
icon-name="exclamation-triangle"
|
|
21
|
+
:closeable="true"
|
|
22
|
+
duration="Infinity"
|
|
23
|
+
:msg="$t('viur.shop.country_change_vat_hint')"
|
|
24
|
+
@on-hide="state.showCountryChangeHint = false"
|
|
25
|
+
/>
|
|
16
26
|
</template>
|
|
17
27
|
|
|
18
28
|
<script setup>
|
|
19
29
|
import {computed, onMounted, reactive, watch} from 'vue'
|
|
20
30
|
import LoadingHandler from './LoadingHandler.vue';
|
|
31
|
+
import ShopAlert from './ShopAlert.vue';
|
|
21
32
|
import ViForm from "@viur/vue-utils/forms/ViForm.vue";
|
|
22
33
|
import {useViurShopStore} from "../shop";
|
|
23
34
|
import {useAddress} from "../composables/address";
|
|
@@ -68,11 +79,14 @@ const state = reactive({
|
|
|
68
79
|
}
|
|
69
80
|
return [state.formtype]
|
|
70
81
|
}),
|
|
71
|
-
language: "de"
|
|
82
|
+
language: "de",
|
|
83
|
+
initialCountry: null,
|
|
84
|
+
showCountryChangeHint: false
|
|
72
85
|
})
|
|
73
86
|
|
|
74
87
|
function formChange(data){
|
|
75
88
|
if (data.name === "country"){
|
|
89
|
+
state.showCountryChangeHint = data.value !== state.initialCountry
|
|
76
90
|
state.language = data.value
|
|
77
91
|
if (state.formtype === 'billing'){
|
|
78
92
|
fetchPaymentData()
|
|
@@ -87,6 +101,7 @@ onMounted(()=>{
|
|
|
87
101
|
}else{
|
|
88
102
|
state.language = shopStore.state.language
|
|
89
103
|
}
|
|
104
|
+
state.initialCountry = state.language
|
|
90
105
|
})
|
|
91
106
|
|
|
92
107
|
|
|
@@ -104,5 +119,7 @@ watch(()=>addressState.billingIsShipping, (newVal,oldVal)=>{
|
|
|
104
119
|
</script>
|
|
105
120
|
|
|
106
121
|
<style scoped>
|
|
107
|
-
|
|
122
|
+
.country-change-hint {
|
|
123
|
+
margin: 1em 0;
|
|
124
|
+
}
|
|
108
125
|
</style>
|
package/src/composables/cart.js
CHANGED
|
@@ -35,26 +35,31 @@ export function useCart() {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
let _fetchCartPromise = null;
|
|
38
39
|
function fetchCart() {
|
|
39
|
-
//
|
|
40
|
+
// Deduplicate parallel calls - return existing promise if one is in flight
|
|
41
|
+
if (_fetchCartPromise) return _fetchCartPromise;
|
|
42
|
+
|
|
40
43
|
shopStore.state.cartIsLoading = true;
|
|
44
|
+
let promise;
|
|
41
45
|
if (shopStore.state.order != null && shopStore.state.order?.cart?.dest.key) {
|
|
42
|
-
// shopStore.state.cartRoot = {};
|
|
43
46
|
shopStore.state.cartRoot = shopStore.state.order.cart.dest;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
shopStore.state.
|
|
47
|
+
promise = fetchCartItems(shopStore.state.cartRoot["key"]);
|
|
48
|
+
} else {
|
|
49
|
+
promise = fetchCartRoot().then(() => {
|
|
50
|
+
if (!shopStore.state.cartRoot?.["key"]) return 0;
|
|
51
|
+
return fetchCartItems(shopStore.state.cartRoot["key"]);
|
|
48
52
|
});
|
|
49
53
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
54
|
+
|
|
55
|
+
_fetchCartPromise = promise.then(() => {
|
|
56
|
+
shopStore.state.cartIsLoading = false;
|
|
57
|
+
shopStore.state.cartReady = true;
|
|
58
|
+
}).finally(() => {
|
|
59
|
+
_fetchCartPromise = null;
|
|
57
60
|
});
|
|
61
|
+
|
|
62
|
+
return _fetchCartPromise;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
function fetchCartRoot(){
|
|
@@ -69,32 +74,30 @@ export function useCart() {
|
|
|
69
74
|
})
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
if (key === shopStore.state.cartRoot["key"]){ // initial
|
|
75
|
-
shopStore.state.cartList = []
|
|
76
|
-
}
|
|
77
|
-
return Request.get(`${shopStore.state.shopApiUrl}/cart_list`,{dataObj:{
|
|
77
|
+
async function _collectCartItems(key, leafs, discounts){
|
|
78
|
+
let resp = await Request.get(`${shopStore.state.shopApiUrl}/cart_list`,{dataObj:{
|
|
78
79
|
cart_key:key
|
|
79
|
-
}})
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if(item.discount){
|
|
88
|
-
shopStore.state.discounts[item.discount.dest.key] = item.discount
|
|
89
|
-
}
|
|
90
|
-
await fetchCartItems(item['key'], parentKey=true)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (parentKey){
|
|
94
|
-
shopStore.state.cartList=shopStore.state.cartList.concat(currentLeafs)
|
|
95
|
-
}else{
|
|
96
|
-
shopStore.state.cartList=currentLeafs
|
|
80
|
+
}})
|
|
81
|
+
let data = await resp.clone().json()
|
|
82
|
+
for (const item of data){
|
|
83
|
+
if (item["skel_type"]==="leaf"){
|
|
84
|
+
leafs.push(item)
|
|
85
|
+
}else{
|
|
86
|
+
if(item.discount){
|
|
87
|
+
discounts[item.discount.dest.key] = item.discount
|
|
97
88
|
}
|
|
89
|
+
await _collectCartItems(item['key'], leafs, discounts)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return resp
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fetchCartItems(key){
|
|
96
|
+
let leafs = []
|
|
97
|
+
let discounts = {}
|
|
98
|
+
return _collectCartItems(key, leafs, discounts).then((resp) => {
|
|
99
|
+
shopStore.state.cartList = leafs
|
|
100
|
+
Object.assign(shopStore.state.discounts, discounts)
|
|
98
101
|
|
|
99
102
|
return resp
|
|
100
103
|
})
|
|
@@ -128,7 +131,7 @@ export function useCart() {
|
|
|
128
131
|
return Request.post(`${shopStore.state.shopApiUrl}/cart_update`, {
|
|
129
132
|
dataObj: removeUndefinedValues(data)
|
|
130
133
|
}).then(async (resp)=>{
|
|
131
|
-
fetchCart()
|
|
134
|
+
await fetchCart()
|
|
132
135
|
return resp
|
|
133
136
|
})
|
|
134
137
|
}
|
|
@@ -143,7 +146,7 @@ export function useCart() {
|
|
|
143
146
|
quantity_mode:quantity_mode
|
|
144
147
|
}}).then(async (resp)=>{
|
|
145
148
|
shopStore.state.cartIsUpdating=false
|
|
146
|
-
fetchCart()
|
|
149
|
+
await fetchCart()
|
|
147
150
|
})
|
|
148
151
|
|
|
149
152
|
}
|
|
@@ -154,46 +157,26 @@ export function useCart() {
|
|
|
154
157
|
parent_cart_key:cart?cart:shopStore.state.cartRoot['key']
|
|
155
158
|
}}).then(async (resp)=>{
|
|
156
159
|
shopStore.state.cartIsUpdating=false
|
|
157
|
-
fetchCart()
|
|
160
|
+
await fetchCart()
|
|
158
161
|
})
|
|
159
162
|
}
|
|
160
163
|
|
|
161
|
-
function addDiscount(code) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
dataObj: {
|
|
165
|
-
code: code,
|
|
166
|
-
},
|
|
167
|
-
})
|
|
168
|
-
.then(async (resp) => {
|
|
169
|
-
let data = await resp.json();
|
|
170
|
-
fetchCart()
|
|
171
|
-
console.log("discount debug", data);
|
|
172
|
-
resolve()
|
|
164
|
+
async function addDiscount(code) {
|
|
165
|
+
let resp = await Request.securePost(`${shopStore.state.shopApiUrl}/discount_add`, {
|
|
166
|
+
dataObj: { code: code },
|
|
173
167
|
})
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
});
|
|
168
|
+
let data = await resp.json();
|
|
169
|
+
await fetchCart()
|
|
170
|
+
return data
|
|
178
171
|
}
|
|
179
172
|
|
|
180
|
-
function removeDiscount(key) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
dataObj: {
|
|
184
|
-
discount_key: key,
|
|
185
|
-
},
|
|
186
|
-
})
|
|
187
|
-
.then(async (resp) => {
|
|
188
|
-
let data = await resp.json();
|
|
189
|
-
fetchCart()
|
|
190
|
-
console.log("discount debug", data);
|
|
191
|
-
resolve()
|
|
173
|
+
async function removeDiscount(key) {
|
|
174
|
+
let resp = await Request.securePost(`${shopStore.state.shopApiUrl}/discount_remove`, {
|
|
175
|
+
dataObj: { discount_key: key },
|
|
192
176
|
})
|
|
193
|
-
.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
});
|
|
177
|
+
let data = await resp.json();
|
|
178
|
+
await fetchCart()
|
|
179
|
+
return data
|
|
197
180
|
}
|
|
198
181
|
|
|
199
182
|
const shippingAddressKey = computed(() => shopStore.state.cartRoot?.['shipping_address']?.['dest']?.['key']);
|
package/src/translations/de.js
CHANGED
|
@@ -61,6 +61,7 @@ export default {
|
|
|
61
61
|
'payment_link': 'Ihr Browser öffnet kein Popup? Dann klicken Sie bitte <a href="{url}" target="_blank">hier</a>.',
|
|
62
62
|
'birthdate': 'Geburtsdatum',
|
|
63
63
|
'missing_birthdate': 'Bei der ausgewählten Bezahlmethode benötigen wir zur Rechnungsadresse noch das Geburtsdatum von <i>{firstname} {lastname}</i>.',
|
|
64
|
+
'country_change_vat_hint': 'Bitte beachten Sie, dass bei Lieferung in ein anderes Land die Mehrwertsteuer abweichen kann.',
|
|
64
65
|
},
|
|
65
66
|
},
|
|
66
67
|
messages: {
|
package/src/translations/en.js
CHANGED
|
@@ -60,6 +60,7 @@ export default {
|
|
|
60
60
|
'payment_link': 'Your browser does not open a popup? Then please click <a href="{url}" target="_blank">here</a>.',
|
|
61
61
|
'birthdate': 'date of birth',
|
|
62
62
|
'missing_birthdate': 'For the selected payment method, we require the date of birth of <i>{firstname} {lastname}</i> in addition to the billing address.',
|
|
63
|
+
'country_change_vat_hint': 'Please note that VAT may differ when shipping to a different country.',
|
|
63
64
|
},
|
|
64
65
|
},
|
|
65
66
|
messages: {
|
package/src/translations/fr.js
CHANGED
|
@@ -61,6 +61,7 @@ export default {
|
|
|
61
61
|
'payment_link': 'Votre navigateur n\'ouvre pas de popup ? Alors cliquez <a href="{url}" target="_blank">ici</a>.',
|
|
62
62
|
'birthdate': 'date de naissance',
|
|
63
63
|
'missing_birthdate': 'Pour le mode de paiement sélectionné, nous avons besoin, en plus de l\'adresse de facturation, de la date de naissance de <i>{firstname} {lastname}</i>.',
|
|
64
|
+
'country_change_vat_hint': 'Veuillez noter que la TVA peut varier en cas de livraison dans un autre pays.',
|
|
64
65
|
},
|
|
65
66
|
},
|
|
66
67
|
messages: {
|