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.
248 lines
8.6 KiB
248 lines
8.6 KiB
<script setup lang="ts">
|
|
import { Edit3 } from 'lucide-vue-next'
|
|
|
|
/**
|
|
* Checkout Page (/checkout)
|
|
*
|
|
* Features:
|
|
* - Requires authentication (middleware: auth)
|
|
* - Shows CheckoutForm with billing address
|
|
* - Shows CartSummary in right sidebar (desktop) / top (mobile)
|
|
* - Redirects to homepage if cart is empty
|
|
* - "Warenkorb bearbeiten" button opens cart sidebar
|
|
* - Creates order on form submit
|
|
* - Redirects to /payment?orderId={orderId} after order creation
|
|
*/
|
|
|
|
// Type for checkout data (matches server schema)
|
|
type CheckoutData = {
|
|
salutation: 'male' | 'female' | 'other'
|
|
firstName: string
|
|
lastName: string
|
|
dateOfBirth: string
|
|
street: string
|
|
postCode: string
|
|
city: string
|
|
countryCode: string
|
|
saveAddress?: boolean
|
|
}
|
|
|
|
definePageMeta({
|
|
middleware: 'auth',
|
|
layout: 'default',
|
|
})
|
|
|
|
const { items, total, itemCount, fetchCart } = useCart()
|
|
const { open: openCart } = useCartUI()
|
|
|
|
// Loading states
|
|
const isLoading = ref(true) // Start as true to prevent premature redirect
|
|
const isCreatingOrder = ref(false)
|
|
const cartLoaded = ref(false) // Track if initial cart fetch completed
|
|
|
|
// Error state
|
|
const error = ref<string | null>(null)
|
|
|
|
// Fetch cart on mount
|
|
onMounted(async () => {
|
|
isLoading.value = true
|
|
try {
|
|
await fetchCart()
|
|
cartLoaded.value = true // Mark cart as loaded
|
|
} catch (err) {
|
|
console.error('Failed to fetch cart:', err)
|
|
error.value = 'Fehler beim Laden des Warenkorbs'
|
|
cartLoaded.value = true // Mark as loaded even on error to allow redirect
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
})
|
|
|
|
// Redirect to homepage if cart is empty (only after cart is loaded)
|
|
watchEffect(() => {
|
|
if (cartLoaded.value && !isLoading.value && itemCount.value === 0) {
|
|
navigateTo('/')
|
|
}
|
|
})
|
|
|
|
// Handle checkout form submission
|
|
async function handleCheckout(checkoutData: CheckoutData) {
|
|
isCreatingOrder.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
// Create order via API
|
|
const response = await $fetch<{
|
|
success: boolean
|
|
orderId: string
|
|
orderNumber: string
|
|
message: string
|
|
}>('/api/orders/create', {
|
|
method: 'POST',
|
|
body: checkoutData,
|
|
})
|
|
|
|
if (response.success) {
|
|
// Redirect to payment page with order ID
|
|
navigateTo(`/payment?orderId=${response.orderId}`)
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Order creation failed:', err)
|
|
error.value =
|
|
err.data?.message || 'Fehler beim Erstellen der Bestellung. Bitte versuche es erneut.'
|
|
} finally {
|
|
isCreatingOrder.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<CommonHeader />
|
|
|
|
<div class="container mx-auto px-4 py-8 max-w-7xl">
|
|
<!-- Page Header -->
|
|
<div class="mb-12 text-center">
|
|
<h1 class="text-4xl font-bold text-white mb-4 md:text-5xl">Zur Kasse</h1>
|
|
<p class="mx-auto max-w-2xl text-lg text-white/80">
|
|
Bitte gib deine Rechnungsadresse ein, um die Bestellung abzuschließen.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Error Alert -->
|
|
<Alert v-if="error" variant="error" class="mb-6">
|
|
<AlertTitle>Fehler</AlertTitle>
|
|
<AlertDescription>{{ error }}</AlertDescription>
|
|
</Alert>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading" class="text-center py-12">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-4 border-white/20 border-t-white mx-auto mb-4" />
|
|
<p class="text-white/60">Lade Warenkorb...</p>
|
|
</div>
|
|
|
|
<!-- Checkout Layout -->
|
|
<div v-else class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<!-- Left Column: Checkout Form (2/3 width on desktop) -->
|
|
<div class="lg:col-span-2">
|
|
<Card class="p-6">
|
|
<h2 class="text-2xl font-bold text-white mb-6">Rechnungsadresse</h2>
|
|
<CheckoutForm :loading="isCreatingOrder" @submit="handleCheckout" />
|
|
</Card>
|
|
</div>
|
|
|
|
<!-- Right Column: Cart Summary (1/3 width on desktop, sticky) -->
|
|
<div class="lg:col-span-1">
|
|
<div class="lg:sticky lg:top-4">
|
|
<!-- Mobile: Show summary at top, Desktop: Show in sidebar -->
|
|
<div class="lg:hidden mb-8">
|
|
<CartSummary :items="items" :total="total" :loading="isCreatingOrder" @checkout="() => { }"
|
|
class="hidden" />
|
|
<!-- Simplified summary for mobile -->
|
|
<Card class="p-4 bg-white/5 border-white/10">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm text-white/60">
|
|
{{ itemCount }} {{ itemCount === 1 ? 'Artikel' : 'Artikel' }}
|
|
</p>
|
|
<p class="text-2xl font-bold text-experimenta-accent">
|
|
{{
|
|
new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
}).format(total)
|
|
}}
|
|
</p>
|
|
</div>
|
|
<button @click="openCart" class="text-sm text-experimenta-accent hover:underline">
|
|
Bearbeiten
|
|
</button>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<!-- Desktop: Full cart summary -->
|
|
<div class="hidden lg:block">
|
|
<Card class="p-6">
|
|
<h2 class="text-xl font-bold text-white mb-4">Deine Bestellung</h2>
|
|
|
|
<!-- Items List -->
|
|
<div class="space-y-3 mb-4 divide-y divide-white/10">
|
|
<div v-for="item in items" :key="item.id"
|
|
class="flex items-start justify-between pt-4 py-2 border-white/10">
|
|
<div class="flex-1">
|
|
<p class="text-sm font-medium text-white">{{ item.product.name }}</p>
|
|
<p class="text-xs text-white/60">
|
|
{{ item.quantity }}x
|
|
{{
|
|
new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
}).format(Number.parseFloat(item.product.price))
|
|
}}
|
|
</p>
|
|
</div>
|
|
<p class="text-sm font-semibold text-white">
|
|
{{
|
|
new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
}).format(item.subtotal)
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Totals -->
|
|
<div class="space-y-2 pt-4 border-t border-white/20">
|
|
<div class="flex items-center justify-between text-white/80">
|
|
<span class="text-sm">Zwischensumme</span>
|
|
<span class="font-medium">
|
|
{{
|
|
new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
}).format(total)
|
|
}}
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center justify-between text-white/60 text-sm">
|
|
<span>inkl. MwSt. (7%)</span>
|
|
<span>
|
|
{{
|
|
new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
}).format(total * (0.07 / 1.07))
|
|
}}
|
|
</span>
|
|
</div>
|
|
<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">
|
|
{{
|
|
new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
}).format(total)
|
|
}}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Cart Link -->
|
|
<div class="pt-6 border-t border-white/20">
|
|
<button @click="openCart"
|
|
class="inline-flex items-center gap-2 text-sm text-experimenta-accent hover:text-experimenta-accent/80 transition-colors">
|
|
<Edit3 :size="16" />
|
|
<span>Warenkorb bearbeiten</span>
|
|
</button>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|