- Refactored CheckoutForm.vue to utilize an extended user type, incorporating additional address fields for improved user data handling. - Updated OrderSummary.vue to conditionally display salutation based on user input. - Standardized error alert styling across multiple pages, changing variant from 'destructive' to 'error' for consistency. - Adjusted button styles in various components to align with the new 'experimenta' variant. These changes aim to improve user experience by ensuring accurate data representation and consistent UI elements across the checkout and order processes.
190 lines
5.2 KiB
Vue
190 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* OrderSummary Component
|
|
*
|
|
* Displays order details including:
|
|
* - Product list with quantities and prices
|
|
* - Subtotal, VAT, and total
|
|
* - Billing address
|
|
*
|
|
* Used on:
|
|
* - Order confirmation page (/bestellung/bestaetigen/[orderId])
|
|
* - Order success page (/bestellung/erfolg/[orderId])
|
|
*/
|
|
|
|
interface OrderItem {
|
|
id: string
|
|
productId: string
|
|
quantity: number
|
|
priceSnapshot: string
|
|
productSnapshot: {
|
|
name: string
|
|
description?: string
|
|
}
|
|
product?: {
|
|
name: string
|
|
imageUrl?: string | null
|
|
}
|
|
subtotal: number
|
|
}
|
|
|
|
interface BillingAddress {
|
|
salutation: 'male' | 'female' | 'other'
|
|
firstName: string
|
|
lastName: string
|
|
dateOfBirth: string
|
|
street: string
|
|
postCode: string
|
|
city: string
|
|
countryCode: string
|
|
}
|
|
|
|
interface Order {
|
|
id: string
|
|
orderNumber: string
|
|
totalAmount: string | number
|
|
status: string
|
|
billingAddress: BillingAddress
|
|
items: OrderItem[]
|
|
createdAt: Date | string
|
|
}
|
|
|
|
interface Props {
|
|
order: Order
|
|
/**
|
|
* Show billing address section
|
|
*/
|
|
showAddress?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
showAddress: true,
|
|
})
|
|
|
|
// Format currency in EUR
|
|
const formatCurrency = (amount: number | string) => {
|
|
const value = typeof amount === 'string' ? Number.parseFloat(amount) : amount
|
|
return new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
}).format(value)
|
|
}
|
|
|
|
// Calculate total from items (for client-side display)
|
|
const total = computed(() => {
|
|
return typeof props.order.totalAmount === 'string'
|
|
? Number.parseFloat(props.order.totalAmount)
|
|
: props.order.totalAmount
|
|
})
|
|
|
|
// Calculate VAT (7% already included in total)
|
|
const vatAmount = computed(() => {
|
|
return total.value * (0.07 / 1.07)
|
|
})
|
|
|
|
// Format salutation
|
|
const formatSalutation = (salutation: string) => {
|
|
const map: Record<string, string> = {
|
|
male: 'Herr',
|
|
female: 'Frau',
|
|
other: 'Keine Angabe',
|
|
}
|
|
return map[salutation] || salutation
|
|
}
|
|
|
|
// Format date
|
|
const formatDate = (date: Date | string) => {
|
|
const d = typeof date === 'string' ? new Date(date) : date
|
|
return new Intl.DateTimeFormat('de-DE', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(d)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-6">
|
|
<!-- Order Header -->
|
|
<div class="pb-4 border-b border-white/20">
|
|
<h2 class="text-2xl font-bold text-white">Bestellübersicht</h2>
|
|
<p class="text-sm text-white/60 mt-1">
|
|
Bestellnummer: <span class="font-mono text-white/80">{{ order.orderNumber }}</span>
|
|
</p>
|
|
<p class="text-sm text-white/60">
|
|
Erstellt am: {{ formatDate(order.createdAt) }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Order Items -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-semibold text-white">Artikel</h3>
|
|
<div class="space-y-3">
|
|
<div
|
|
v-for="item in order.items"
|
|
:key="item.id"
|
|
class="flex items-start justify-between gap-4 p-4 rounded-lg bg-white/5 border border-white/10"
|
|
>
|
|
<div class="flex-1">
|
|
<h4 class="font-medium text-white">
|
|
{{ item.productSnapshot?.name || item.product?.name }}
|
|
</h4>
|
|
<p class="text-sm text-white/60 mt-1">
|
|
{{ item.quantity }}x {{ formatCurrency(item.priceSnapshot) }}
|
|
</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="font-semibold text-white">
|
|
{{ formatCurrency(item.subtotal) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Price Breakdown -->
|
|
<div class="space-y-3 pt-4 border-t border-white/20">
|
|
<!-- Subtotal -->
|
|
<div class="flex items-center justify-between text-white/80">
|
|
<span class="text-sm">Zwischensumme</span>
|
|
<span class="font-medium">{{ formatCurrency(total) }}</span>
|
|
</div>
|
|
|
|
<!-- VAT (included) -->
|
|
<div class="flex items-center justify-between text-white/60 text-sm">
|
|
<span>inkl. MwSt. (7%)</span>
|
|
<span>{{ formatCurrency(vatAmount) }}</span>
|
|
</div>
|
|
|
|
<!-- Total -->
|
|
<div class="flex items-center justify-between pt-3 border-t border-white/20">
|
|
<span class="text-lg font-bold text-white">Gesamt</span>
|
|
<span class="text-2xl font-bold text-experimenta-accent">
|
|
{{ formatCurrency(total) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Billing Address -->
|
|
<div v-if="showAddress" class="space-y-3 pt-6 border-t border-white/20">
|
|
<h3 class="text-lg font-semibold text-white">Rechnungsadresse</h3>
|
|
<div class="p-4 rounded-lg bg-white/5 border border-white/10 space-y-1 text-sm">
|
|
<p class="text-white">
|
|
<template v-if="order.billingAddress.salutation !== 'other'">
|
|
{{ formatSalutation(order.billingAddress.salutation) }}
|
|
</template>
|
|
{{ order.billingAddress.firstName }}
|
|
{{ order.billingAddress.lastName }}
|
|
</p>
|
|
<p class="text-white/80">{{ order.billingAddress.street }}</p>
|
|
<p class="text-white/80">
|
|
{{ order.billingAddress.postCode }} {{ order.billingAddress.city }}
|
|
</p>
|
|
<p class="text-white/80">{{ order.billingAddress.countryCode }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|