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:
Bastian Masanek
2025-11-03 15:38:16 +01:00
parent 47fe14c6cc
commit 527379a2cd
44 changed files with 4957 additions and 142 deletions

View 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>