You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
189 lines
5.2 KiB
189 lines
5.2 KiB
<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>
|
|
|