Enhance checkout flow with new components and validation
- Added AddressForm and CheckoutForm components for user input during checkout. - Implemented validation using Zod and VeeValidate for billing address fields. - Created OrderSummary and MockPayPalButton components for order confirmation and payment simulation. - Updated CartSheet and CartSidebar to navigate to the new checkout page at '/kasse'. - Introduced new API endpoints for validating checkout data and creating orders. - Enhanced user experience with responsive design and error handling. These changes complete the checkout functionality, allowing users to enter billing information, simulate payment, and confirm orders.
This commit is contained in:
187
app/components/Order/OrderSummary.vue
Normal file
187
app/components/Order/OrderSummary.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<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">
|
||||
{{ formatSalutation(order.billingAddress.salutation) }}
|
||||
{{ 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>
|
||||
Reference in New Issue
Block a user