@shopbite-de/storefront 1.20.0 → 1.21.0

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.
@@ -34,8 +34,15 @@ const emit = defineEmits(["go-to-cart"]);
34
34
  </div>
35
35
  <div class="flex flex-col gap-4">
36
36
  <div class="flex flex-row justify-between">
37
- <div>Versandkosten:</div>
38
- <div>{{ getFormattedPrice(shippingTotal) }}</div>
37
+ <template v-if="shippingTotal === 0">
38
+ <div class="text-success font-medium">
39
+ Versandkostenfreie Lieferung
40
+ </div>
41
+ </template>
42
+ <template v-else>
43
+ <div>Versandkosten:</div>
44
+ <div>{{ getFormattedPrice(shippingTotal) }}</div>
45
+ </template>
39
46
  </div>
40
47
  <div class="flex flex-row justify-between font-bold">
41
48
  <div>Summe:</div>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import QuickView from "~/components/Cart/QuickView.vue";
3
3
  import { useIntervalFn } from "@vueuse/core";
4
+ import { useOrderPayment } from "@shopware/composables";
4
5
 
5
6
  const { createOrder, selectedPaymentMethod, selectedShippingMethod } =
6
7
  useCheckout();
@@ -9,6 +10,10 @@ const { isLoggedIn, isGuestSession, refreshUser } = useUser();
9
10
  const { isCheckoutEnabled, refresh } = useShopBiteConfig();
10
11
  const { trackOrder } = useTrackEvent();
11
12
 
13
+ const {
14
+ public: { storeUrl },
15
+ } = useRuntimeConfig();
16
+
12
17
  const toast = useToast();
13
18
 
14
19
  onMounted(() => {
@@ -25,21 +30,42 @@ onMounted(() => {
25
30
 
26
31
  useIntervalFn(refresh, 10000);
27
32
 
28
- async function handleCreateOrder() {
29
- const order = await createOrder({
30
- customerComment: "Wunschlieferzeit: " + selectedDeliveryTime.value,
31
- });
32
-
33
- trackOrder(order);
33
+ const createdOrder = ref<Awaited<ReturnType<typeof createOrder>> | null>(null);
34
+ const { handlePayment, paymentUrl } = useOrderPayment(
35
+ computed(() => createdOrder.value),
36
+ );
34
37
 
35
- await refreshCart();
36
- toast.add({
37
- title: "Bestellung aufgegeben!",
38
- icon: "i-lucide-shopping-cart",
39
- color: "success",
40
- progress: false,
41
- });
42
- navigateTo("/order/" + order.id);
38
+ async function handleCreateOrder() {
39
+ isPlacingOrder.value = true;
40
+ try {
41
+ const order = await createOrder({
42
+ customerComment: "Wunschlieferzeit: " + selectedDeliveryTime.value,
43
+ });
44
+
45
+ trackOrder(order);
46
+ createdOrder.value = order;
47
+
48
+ await handlePayment(
49
+ `${storeUrl}/bestellung/${order.id}/erfolg`,
50
+ `${storeUrl}/bestellung/${order.id}/fehler`,
51
+ );
52
+
53
+ if (paymentUrl.value) {
54
+ await navigateTo(paymentUrl.value, { external: true });
55
+ return;
56
+ }
57
+
58
+ await refreshCart();
59
+ toast.add({
60
+ title: "Bestellung aufgegeben!",
61
+ icon: "i-lucide-shopping-cart",
62
+ color: "success",
63
+ progress: false,
64
+ });
65
+ navigateTo(`/bestellung/${order.id}/erfolg`);
66
+ } finally {
67
+ isPlacingOrder.value = false;
68
+ }
43
69
  }
44
70
 
45
71
  const customerDataAvailable = computed<boolean>(
@@ -58,6 +84,7 @@ const isValidToProceed = computed(
58
84
  shippingAndPaymentSet.value,
59
85
  );
60
86
 
87
+ const isPlacingOrder = ref(false);
61
88
  const selectedDeliveryTime = ref("");
62
89
  const isValidTime = ref(true);
63
90
 
@@ -106,7 +133,8 @@ const checkoutButtonLabel = computed<string>(() => {
106
133
  <CheckoutVoucherInput />
107
134
  <UButton
108
135
  :icon="isValidToProceed ? 'i-lucide-shopping-cart' : 'i-lucide-lock'"
109
- :disabled="!isValidToProceed"
136
+ :disabled="!isValidToProceed || isPlacingOrder"
137
+ :loading="isPlacingOrder"
110
138
  :label="checkoutButtonLabel"
111
139
  size="xl"
112
140
  block
@@ -1,6 +1,5 @@
1
1
  <script setup lang="ts">
2
2
  import type { Schemas } from "#shopware";
3
- import type { TableColumn } from "#ui/components/Table.vue";
4
3
 
5
4
  const props = defineProps<{
6
5
  order: Schemas["Order"];
@@ -14,89 +13,118 @@ const { getFormattedPrice } = usePrice({
14
13
  localeCode: "de-DE",
15
14
  });
16
15
 
17
- const isLoadingData = ref(true);
16
+ const lineItems = computed(() =>
17
+ order.value?.lineItems?.filter(
18
+ (item: Schemas["OrderLineItem"]) => item.parentId === null,
19
+ ),
20
+ );
18
21
 
19
- const columns: TableColumn<Schemas["OrderLineItem"]>[] = [
20
- {
21
- accessorKey: "payload.productNumber",
22
- header: "#",
23
- },
24
- {
25
- accessorKey: "label",
26
- header: "Name",
27
- },
28
- {
29
- accessorKey: "unitPrice",
30
- header: "Preis",
31
- cell: ({ row }) => {
32
- return getFormattedPrice(row.getValue("unitPrice"));
33
- },
34
- },
35
- {
36
- accessorKey: "quantity",
37
- header: "Anzahl",
38
- },
39
- {
40
- accessorKey: "totalPrice",
41
- header: () => h("div", { class: "text-right" }, "Gesamt"),
42
- cell: ({ row }) => {
43
- const formatted = getFormattedPrice(row.getValue("totalPrice"));
22
+ const paymentState = computed(
23
+ () => order.value?.transactions?.at(-1)?.stateMachineState,
24
+ );
44
25
 
45
- return h("div", { class: "text-right" }, formatted);
46
- },
47
- },
48
- ];
26
+ type BadgeColor =
27
+ | "success"
28
+ | "error"
29
+ | "warning"
30
+ | "info"
31
+ | "neutral"
32
+ | "primary";
49
33
 
50
- onMounted(() => {
51
- isLoadingData.value = false;
52
- });
53
-
54
- const columnRows = computed(() => {
55
- return order.value?.lineItems?.filter(
56
- (lineItem: Schemas["OrderLineItem"]) => lineItem.parentId === null,
57
- );
34
+ const paymentStateColor = computed((): BadgeColor => {
35
+ switch (paymentState.value?.technicalName) {
36
+ case "paid":
37
+ case "authorized":
38
+ return "success";
39
+ case "failed":
40
+ case "cancelled":
41
+ case "chargeback":
42
+ return "error";
43
+ case "open":
44
+ case "reminded":
45
+ case "unconfirmed":
46
+ return "warning";
47
+ case "in_progress":
48
+ return "info";
49
+ default:
50
+ return "neutral";
51
+ }
58
52
  });
59
53
  </script>
60
54
 
61
55
  <template>
62
- <div>
63
- <div class="flex flex-row justify-between">
64
- <UBadge variant="outline" color="neutral" size="xl"
65
- >Status: {{ status }}</UBadge
56
+ <div class="flex flex-col gap-6">
57
+ <div class="flex flex-wrap gap-2">
58
+ <UBadge variant="subtle" color="neutral" icon="i-lucide-package">
59
+ {{ status }}
60
+ </UBadge>
61
+ <UBadge
62
+ v-if="order?.deliveries?.[0]?.shippingMethod?.name"
63
+ variant="subtle"
64
+ color="neutral"
65
+ icon="i-lucide-truck"
66
66
  >
67
- <UBadge variant="outline" color="neutral" size="xl"
68
- >Versandart: {{ order?.deliveries?.[0]?.shippingMethod?.name }}</UBadge
67
+ {{ order.deliveries[0].shippingMethod.name }}
68
+ </UBadge>
69
+ <UBadge
70
+ v-if="paymentState"
71
+ variant="subtle"
72
+ :color="paymentStateColor"
73
+ icon="i-lucide-credit-card"
69
74
  >
75
+ {{ paymentState.translated?.name ?? paymentState.name }}
76
+ </UBadge>
70
77
  </div>
71
- <UTable
72
- :columns="columns"
73
- :loading="isLoadingData"
74
- loading-color="primary"
75
- loading-animation="carousel"
76
- :data="columnRows"
77
- class="flex-1"
78
- />
79
- <div class="flex flex-col items-end w-full pr-4">
80
- <div>Lieferkosten: {{ getFormattedPrice(order?.shippingTotal) }}</div>
81
- <div>Gesamtkosten Netto: {{ getFormattedPrice(order?.amountNet) }}</div>
82
- <div v-for="tax in order?.price.calculatedTaxes" :key="tax.taxRate">
83
- inkl. {{ tax.taxRate }}% MwSt. {{ getFormattedPrice(tax.tax) }}
78
+
79
+ <ul class="flex flex-col divide-y divide-default">
80
+ <li
81
+ v-for="item in lineItems"
82
+ :key="item.id"
83
+ class="flex items-start gap-4 py-4 first:pt-0 last:pb-0"
84
+ >
85
+ <div
86
+ class="size-10 rounded-md bg-elevated flex items-center justify-center shrink-0 text-sm font-semibold text-muted"
87
+ >
88
+ {{ item.quantity }}×
89
+ </div>
90
+ <div class="flex flex-col gap-0.5 flex-1 min-w-0">
91
+ <p class="font-medium truncate">{{ item.label }}</p>
92
+ <p
93
+ v-if="item.payload?.productNumber"
94
+ class="text-xs text-muted truncate"
95
+ >
96
+ #{{ item.payload.productNumber }}
97
+ </p>
98
+ </div>
99
+ <p class="font-semibold shrink-0">
100
+ {{ getFormattedPrice(item.totalPrice) }}
101
+ </p>
102
+ </li>
103
+ </ul>
104
+
105
+ <USeparator />
106
+
107
+ <div class="flex flex-col gap-2 text-sm">
108
+ <div class="flex justify-between text-muted">
109
+ <span>Lieferkosten</span>
110
+ <span>{{ getFormattedPrice(order?.shippingTotal) }}</span>
84
111
  </div>
85
- <div class="font-bold">
86
- Gesamtkosten Brutto: {{ getFormattedPrice(order?.amountTotal) }}
112
+ <div class="flex justify-between text-muted">
113
+ <span>Netto</span>
114
+ <span>{{ getFormattedPrice(order?.amountNet) }}</span>
87
115
  </div>
88
- </div>
89
- <div class="p-2 text-balance mt-4">
90
- <div class="text-sm text-muted">
91
- Bei Fragen oder Problemen wenden Sie sich bitte telefonisch an uns.
116
+ <div
117
+ v-for="tax in order?.price?.calculatedTaxes"
118
+ :key="tax.taxRate"
119
+ class="flex justify-between text-muted"
120
+ >
121
+ <span>MwSt. {{ tax.taxRate }}%</span>
122
+ <span>{{ getFormattedPrice(tax.tax) }}</span>
123
+ </div>
124
+ <div class="flex justify-between font-semibold text-base pt-1">
125
+ <span>Gesamt</span>
126
+ <span>{{ getFormattedPrice(order?.amountTotal) }}</span>
92
127
  </div>
93
- <UButton
94
- label="Anrufen"
95
- variant="outline"
96
- color="primary"
97
- to="tel:+4917623456789"
98
- icon="i-heroicons-phone"
99
- />
100
128
  </div>
101
129
  </div>
102
130
  </template>
@@ -128,6 +128,12 @@ export function useAddToCart() {
128
128
  color: "primary",
129
129
  progress: true,
130
130
  duration: 2000,
131
+ actions: [
132
+ {
133
+ label: "Zum Warenkorb",
134
+ to: "/bestellung/warenkorb",
135
+ },
136
+ ],
131
137
  });
132
138
  }
133
139
 
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ const {
3
+ public: { site },
4
+ } = useRuntimeConfig();
5
+
6
+ useSeoMeta({
7
+ title: `Zahlung erfolgreich | ${site.name}`,
8
+ robots: "noindex, nofollow",
9
+ });
10
+
11
+ const route = useRoute();
12
+ const orderId = route.params.id as string;
13
+
14
+ const { refreshCart } = useCart();
15
+
16
+ onMounted(async () => {
17
+ await refreshCart();
18
+ });
19
+
20
+ const links = [
21
+ {
22
+ label: "Zur Bestellung",
23
+ to: `/bestellung/${orderId}`,
24
+ icon: "i-lucide-receipt",
25
+ size: "xl" as const,
26
+ },
27
+ ];
28
+ </script>
29
+
30
+ <template>
31
+ <UPageSection
32
+ icon="i-lucide-circle-check"
33
+ title="Bestellung erfolgreich erstellt!"
34
+ description="Wir bereiten deine Bestellung jetzt vor."
35
+ :links="links"
36
+ />
37
+ </template>
@@ -0,0 +1,128 @@
1
+ <script setup lang="ts">
2
+ import { useOrderPayment, useOrderDetails } from "@shopware/composables";
3
+ import type { Schemas } from "#shopware";
4
+
5
+ const {
6
+ public: { site, storeUrl },
7
+ } = useRuntimeConfig();
8
+
9
+ useSeoMeta({
10
+ title: `Zahlung fehlgeschlagen | ${site.name}`,
11
+ robots: "noindex, nofollow",
12
+ });
13
+
14
+ const route = useRoute();
15
+ const orderId = route.params.id as string;
16
+
17
+ const { order, loadOrderDetails, status } = useOrderDetails(orderId);
18
+ const { paymentMethods, getPaymentMethods } = useCheckout();
19
+
20
+ onMounted(async () => {
21
+ await Promise.all([loadOrderDetails(), getPaymentMethods()]);
22
+ });
23
+
24
+ const orderRef = computed(() => order.value);
25
+ const { handlePayment, paymentUrl, changePaymentMethod } =
26
+ useOrderPayment(orderRef);
27
+
28
+ const currentPaymentMethodId = computed(
29
+ () => order.value?.transactions?.at(-1)?.paymentMethodId ?? "",
30
+ );
31
+ const selectedPaymentMethodId = ref("");
32
+
33
+ watch(
34
+ currentPaymentMethodId,
35
+ (id) => {
36
+ if (id && !selectedPaymentMethodId.value) {
37
+ selectedPaymentMethodId.value = id;
38
+ }
39
+ },
40
+ { immediate: true },
41
+ );
42
+
43
+ const selectablePaymentMethods = computed(() =>
44
+ paymentMethods.value?.map((m: Schemas["PaymentMethod"]) => ({
45
+ label: m.distinguishableName ?? m.name,
46
+ value: m.id,
47
+ })),
48
+ );
49
+
50
+ const isRetrying = ref(false);
51
+
52
+ async function retryPayment() {
53
+ isRetrying.value = true;
54
+ try {
55
+ if (selectedPaymentMethodId.value !== currentPaymentMethodId.value) {
56
+ await changePaymentMethod(selectedPaymentMethodId.value);
57
+ }
58
+
59
+ await handlePayment(
60
+ `${storeUrl}/bestellung/${orderId}/erfolg`,
61
+ `${storeUrl}/bestellung/${orderId}/fehler`,
62
+ );
63
+
64
+ if (paymentUrl.value) {
65
+ await navigateTo(paymentUrl.value, { external: true });
66
+ } else {
67
+ await navigateTo(`/bestellung/${orderId}/erfolg`);
68
+ }
69
+ } finally {
70
+ isRetrying.value = false;
71
+ }
72
+ }
73
+ </script>
74
+
75
+ <template>
76
+ <UPageSection>
77
+ <div class="flex flex-col gap-8">
78
+ <UPageHero
79
+ icon="i-lucide-circle-x"
80
+ title="Zahlung fehlgeschlagen"
81
+ description="Bei der Verarbeitung deiner Zahlung ist ein Fehler aufgetreten. Wähle eine Zahlungsmethode und versuche es erneut."
82
+ />
83
+
84
+ <USeparator />
85
+
86
+ <div v-if="order" class="grid grid-cols-1 md:grid-cols-2 gap-8">
87
+ <div class="flex flex-col gap-4">
88
+ <p class="text-sm font-semibold text-muted uppercase tracking-wide">
89
+ Bestellung {{ order.orderNumber }}
90
+ </p>
91
+ <UCard>
92
+ <OrderDetail :order="order" :status="status ?? ''" />
93
+ </UCard>
94
+ </div>
95
+
96
+ <div class="flex flex-col gap-6">
97
+ <div class="flex flex-col gap-3">
98
+ <p class="text-sm font-semibold text-muted uppercase tracking-wide">
99
+ Zahlungsmethode
100
+ </p>
101
+ <URadioGroup
102
+ v-model="selectedPaymentMethodId"
103
+ :items="selectablePaymentMethods"
104
+ variant="card"
105
+ />
106
+ </div>
107
+
108
+ <UButton
109
+ label="Jetzt bezahlen"
110
+ icon="i-lucide-refresh-cw"
111
+ size="xl"
112
+ block
113
+ :loading="isRetrying"
114
+ :disabled="!selectedPaymentMethodId"
115
+ @click="retryPayment"
116
+ />
117
+ </div>
118
+ </div>
119
+
120
+ <div v-else class="flex justify-center py-16">
121
+ <UIcon
122
+ name="i-lucide-loader-circle"
123
+ class="size-10 animate-spin text-primary"
124
+ />
125
+ </div>
126
+ </div>
127
+ </UPageSection>
128
+ </template>
@@ -55,6 +55,26 @@ const links = ref<ButtonProps[]>([
55
55
  />
56
56
  <UPageBody>
57
57
  <OrderDetail :order="order" :status="status ?? 'laden...'" />
58
+ <UCard class="mt-6">
59
+ <div
60
+ class="flex flex-col sm:flex-row items-start sm:items-center gap-4"
61
+ >
62
+ <div class="flex-1">
63
+ <p class="font-semibold">Stimmt etwas nicht?</p>
64
+ <p class="text-sm text-muted mt-0.5">
65
+ Ruf uns sofort an, damit wir deine Bestellung noch rechtzeitig
66
+ korrigieren können.
67
+ </p>
68
+ </div>
69
+ <UButton
70
+ label="Jetzt anrufen"
71
+ color="primary"
72
+ to="tel:+4917623456789"
73
+ icon="i-lucide-phone"
74
+ shrink-0
75
+ />
76
+ </div>
77
+ </UCard>
58
78
  </UPageBody>
59
79
  </UContainer>
60
80
 
@@ -31,14 +31,24 @@ const items = computed(
31
31
  ] satisfies StepperItem[],
32
32
  );
33
33
 
34
+ const route = useRoute();
35
+ const isPaymentReturnRoute = computed(() =>
36
+ /^\/bestellung\/[0-9a-f]{32}(\/erfolg|\/fehler)?$/.test(route.path),
37
+ );
34
38
  watch(step, (newStep) => {
39
+ if (isPaymentReturnRoute.value) {
40
+ return;
41
+ }
35
42
  navigateTo(stepRoutes[newStep]);
36
43
  });
37
44
  </script>
38
45
 
39
46
  <template>
40
47
  <UPageSection>
41
- <UStepper ref="stepper" v-model="step" :items="items" size="lg">
48
+ <template v-if="isPaymentReturnRoute">
49
+ <NuxtPage />
50
+ </template>
51
+ <UStepper v-else ref="stepper" v-model="step" :items="items" size="lg">
42
52
  <template #content>
43
53
  <NuxtPage />
44
54
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopbite-de/storefront",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "main": "nuxt.config.ts",
5
5
  "description": "Shopware storefront for food delivery shops",
6
6
  "keywords": [