@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.
@@ -20,11 +20,9 @@ jobs:
20
20
  uses: actions/checkout@v4
21
21
  - uses: actions/setup-node@v4
22
22
  with:
23
- node-version: 22
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viur/shop-components",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Frontend Vue components for the shop module of ViUR",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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.showNodes && item.skel_type === 'leaf') || shopStore.state.showNodes">
7
- <div class="viur-shop-cart-sidebar-summary-item-amount" v-if="item.skel_type === 'leaf'">
8
- {{ item.quantity }}&nbsp;&times;
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 }}&nbsp;&times;
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
- <div class="viur-shop-cart-sidebar-summary-item-name" v-html="item.skel_type === 'node' ? item.name : item.shop_name"></div>
11
- <div class="viur-shop-cart-sidebar-summary-item-price">
12
- <sl-format-number lang="de" type="currency" currency="EUR"
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
- </div>
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 v-html="$t('viur.shop.summary_discount')"></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>
@@ -35,26 +35,31 @@ export function useCart() {
35
35
  }
36
36
 
37
37
 
38
+ let _fetchCartPromise = null;
38
39
  function fetchCart() {
39
- //first fetch root then fetchItems for this root
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
- return fetchCartItems(shopStore.state.cartRoot["key"]).then(() => { // TODO: duplicate code
46
- shopStore.state.cartIsLoading = false;
47
- shopStore.state.cartReady = true;
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
- shopStore.state.discounts = {}
51
- return fetchCartRoot().then(() => {
52
- if (!shopStore.state.cartRoot?.["key"]) return 0;
53
- fetchCartItems(shopStore.state.cartRoot["key"]).then(() => { // TODO: duplicate code
54
- shopStore.state.cartIsLoading = false;
55
- shopStore.state.cartReady = true;
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 fetchCartItems(key, parentKey=null){
73
- //fetch cart items
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
- }}).then(async( resp) =>{
80
- let data = await resp.clone().json()
81
-
82
- let currentLeafs = []
83
- for (const item of data){
84
- if (item["skel_type"]==="leaf"){
85
- currentLeafs.push(item)
86
- }else{
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
- return new Promise((resolve, reject) => {
163
- Request.securePost(`${shopStore.state.shopApiUrl}/discount_add`, {
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
- .catch((error) => {
175
- reject(error);
176
- });
177
- });
168
+ let data = await resp.json();
169
+ await fetchCart()
170
+ return data
178
171
  }
179
172
 
180
- function removeDiscount(key) {
181
- return new Promise((resolve, reject) => {
182
- Request.securePost(`${shopStore.state.shopApiUrl}/discount_remove`, {
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
- .catch((error) => {
194
- reject(error);
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']);
@@ -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}&nbsp;{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: {
@@ -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}&nbsp;{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: {
@@ -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}&nbsp;{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: {