diff --git a/src/app/coffee-abonnements/page.tsx b/src/app/coffee-abonnements/page.tsx new file mode 100644 index 0000000..e068fb5 --- /dev/null +++ b/src/app/coffee-abonnements/page.tsx @@ -0,0 +1,360 @@ +'use client'; +import React, { useState, useMemo } from 'react'; +import PageLayout from '../components/PageLayout'; + +interface Coffee { + id: string; + name: string; + description: string; + pricePer10: number; // price for 10 units + image: string; // added +} + +const coffees: Coffee[] = [ + { id: 'espresso', name: 'Espresso Roast', description: 'Kräftig und intensiv – dunkle Schokolade, geröstete Gewürze und ein langer, satt-cremiger Nachhall. Ideal für morgens oder den energiereichen Nachmittagsschub.', pricePer10: 12, image: 'https://images.unsplash.com/photo-1509042239860-f550ce710b93?auto=format&fit=crop&w=400&q=60' }, + { id: 'crema', name: 'Caffè Crema', description: 'Ausgewogen & mild – samtiger Körper mit hellen Honig- und Mandelnoten. Ein sanfter, alltagstauglicher Kaffee für jede Uhrzeit und jeden Geschmack.', pricePer10: 11, image: 'https://images.unsplash.com/photo-1524079429932-1e9f39f1f268?auto=format&fit=crop&w=400&q=60' }, + { id: 'colombia', name: 'Colombia Single Origin', description: 'Fruchtige Noten – rote Beeren, leichte Zitrus-Säure und ein sauberer, heller Abgang. Gewaschen auf Höhenlagen, perfekt für Filter und French Press.', pricePer10: 13, image: 'https://images.unsplash.com/photo-1511920170033-f8396924c348?auto=format&fit=crop&w=400&q=60' }, + { id: 'ethiopia', name: 'Ethiopia Sidamo', description: 'Florale Eleganz – Jasmin, Bergamotte und eine feine Tee-ähnliche Struktur. Komplex und duftend, ideal für Genießer, die Nuancen entdecken möchten.', pricePer10: 14, image: 'https://images.unsplash.com/photo-1510626176961-4b57d4b273c4?auto=format&fit=crop&w=400&q=60' }, + { id: 'sumatra', name: 'Sumatra Mandheling', description: 'Würzig & erdig – tiefer Körper, Kakao, Kräuter und dezente Tabaknoten. Vollmundig und schwer, hervorragend für Espresso-Mischungen.', pricePer10: 13, image: 'https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=400&q=60' }, + { id: 'kenya', name: 'Kenya AA', description: 'Lebendige Säure – schwarze Johannisbeere, saftige Traube und spritzige Frische. Klar konturiert und kraftvoll, toll als Pour Over.', pricePer10: 15, image: 'https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=400&q=60' }, + { id: 'brazil', name: 'Brazil Santos', description: 'Nussig & schokoladig – weiche Textur, geröstete Haselnuss und Milchschokolade. Sehr balanciert und vielseitig für Espresso & Milchgetränke.', pricePer10: 11, image: 'https://images.unsplash.com/photo-1509042239860-f550ce710b93?auto=format&fit=crop&w=400&q=60' }, + { id: 'italian', name: 'Italian Roast', description: 'Dunkel & ölig – karamellisierte Bitterstoffe, Rauch und intensive Tiefe. Klassisches Profil für kräftigen Cappuccino oder Latte.', pricePer10: 12, image: 'https://images.unsplash.com/photo-1512568400610-62da28bc8a13?auto=format&fit=crop&w=400&q=60' }, + { id: 'house', name: 'House Blend', description: 'Harmonisch & rund – milde Süße, ausgewogene Säure und sanfter Körper. Entwickelt als universeller Alltagskaffee für jeden Zubereitungsstil.', pricePer10: 10, image: 'https://images.unsplash.com/photo-1461988091159-110d53f20702?auto=format&fit=crop&w=400&q=60' }, + { id: 'decaf', name: 'Decaf Blend', description: 'Voller Geschmack – ohne Koffein. Kakao, leichte Karamell-Noten und sanfter Abgang. Entkoffeiniert im schonenden CO₂-Verfahren.', pricePer10: 12, image: 'https://images.unsplash.com/photo-1497935586351-b67a49e012bf?auto=format&fit=crop&w=400&q=60' }, + { id: 'guatemala', name: 'Guatemala Huehuetenango', description: 'Karamell & Kakao – balanciert mit einer feinen Pflaumenfrucht. Angenehme Süße und klare Struktur – großartig als Filter und Chemex.', pricePer10: 14, image: 'https://images.unsplash.com/photo-1509978778153-491c9a09a1ab?auto=format&fit=crop&w=400&q=60' }, + { id: 'limited', name: 'Limited Reserve', description: 'Exklusiv & komplex – limitierte Mikrolots mit seltenen floralen und tropischen Noten. Für besondere Momente und anspruchsvolle Tassenprofile.', pricePer10: 18, image: 'https://images.unsplash.com/photo-1541167767034-41d7a96c58f1?auto=format&fit=crop&w=400&q=60' } +]; + +export default function CoffeeAbonnementPage() { + const [selections, setSelections] = useState>({}); + const [bump, setBump] = useState>({}); // added: bump animation flags + const [form, setForm] = useState({ + firstName: '', + lastName: '', + email: '', + street: '', + postalCode: '', + city: '', + frequency: 'monatlich', + startDate: '' + }); + const TAX_RATE = 0.07; // 7% Steuer + + const selectedEntries = useMemo( + () => + Object.entries(selections).map(([id, qty]) => ({ + coffee: coffees.find(c => c.id === id)!, + quantity: qty + })), + [selections] + ); + + const totalPrice = useMemo( + () => + selectedEntries.reduce( + (sum, entry) => sum + (entry.quantity / 10) * entry.coffee.pricePer10, + 0 + ), + [selectedEntries] + ); + const taxAmount = useMemo(() => totalPrice * TAX_RATE, [totalPrice]); + const totalWithTax = useMemo(() => totalPrice + taxAmount, [totalPrice, taxAmount]); + + const canSubmit = + selectedEntries.length > 0 && + Object.values(form).every(v => v.trim() !== ''); + + const handleInput = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setForm(prev => ({ ...prev, [name]: value })); + }; + + const toggleCoffee = (id: string) => { + setSelections(prev => { + const copy = { ...prev }; + if (id in copy) { + delete copy[id]; + } else { + copy[id] = 10; // default start quantity + } + return copy; + }); + }; + + const changeQuantity = (id: string, delta: number) => { + setSelections(prev => { + if (!(id in prev)) return prev; + const next = prev[id] + delta; + if (next < 10 || next > 120) return prev; + const updated = { ...prev, [id]: next }; + setBump(b => ({ ...b, [id]: true })); // trigger bump animation + setTimeout(() => setBump(b => ({ ...b, [id]: false })), 250); + return updated; + }); + }; + + return ( + +
+

Kaffee Abonnement konfigurieren

+ + {/* Section 1: Multi coffee selection + per-coffee quantity */} +
+

1. Kaffeesorten & Mengen wählen

+
+ {coffees.map(coffee => { + const active = coffee.id in selections; + const qty = selections[coffee.id] || 0; + return ( +
+
+ {coffee.name} + {/* CHANGED: enhanced price badge */} +
+ + €{coffee.pricePer10} + + + pro 10 Stk + +
+
+
+

{coffee.name}

+
+

+ {coffee.description} +

+ + {active && ( +
+
+ Menge (10–120) + + {qty} Stk + +
+
+ +
+ + changeQuantity( + coffee.id, + parseInt(e.target.value, 10) - qty + ) + } + className="w-full appearance-none cursor-pointer bg-transparent" + style={{ + background: + 'linear-gradient(to right,#059669 0%,#059669 ' + + ((qty - 10) / (120 - 10)) * 100 + + '%,#e5e7eb ' + + ((qty - 10) / (120 - 10)) * 100 + + '%,#e5e7eb 100%)', + height: '6px', + borderRadius: '999px' + }} + /> +
+ +
+
+ Subtotal + + €{((qty / 10) * coffee.pricePer10).toFixed(2)} + +
+
+ )} +
+ ); + })} +
+
+ + {/* Section 2: Form (light styling) */} +
+

2. Deine Daten

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Section 3: Summary (light styling) */} +
+

3. Zusammenfassung

+
+ {selectedEntries.length === 0 && ( +

Noch keine Kaffees ausgewählt.

+ )} + {selectedEntries.map(entry => ( +
+
+ {entry.coffee.name} + + {entry.quantity} Stk • €{entry.coffee.pricePer10}/10 + +
+
+ €{((entry.quantity / 10) * entry.coffee.pricePer10).toFixed(2)} +
+
+ ))} +
+ Gesamt (Netto) + €{totalPrice.toFixed(2)} +
+
+ Steuer ({(TAX_RATE * 100).toFixed(0)}%) + €{taxAmount.toFixed(2)} +
+
+ Gesamt inkl. Steuer + €{totalWithTax.toFixed(2)} +
+ + {!canSubmit && ( +

+ Bitte mindestens eine Kaffeesorte wählen und alle Felder ausfüllen. +

+ )} +
+
+
+
+ ); +} diff --git a/src/app/components/PageLayout.tsx b/src/app/components/PageLayout.tsx index a7e071d..4bc61b8 100644 --- a/src/app/components/PageLayout.tsx +++ b/src/app/components/PageLayout.tsx @@ -25,7 +25,7 @@ export default function PageLayout({ const isMobile = isMobileDevice(); return ( -
+
{showHeader && (
diff --git a/src/app/components/UserDetailModal.tsx b/src/app/components/UserDetailModal.tsx index 246cf15..ddee6dd 100644 --- a/src/app/components/UserDetailModal.tsx +++ b/src/app/components/UserDetailModal.tsx @@ -46,6 +46,11 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated const [selectedStatus, setSelectedStatus] = useState('pending') const token = useAuthStore(state => state.accessToken) + // Contract preview state (lazy-loaded) + const [previewLoading, setPreviewLoading] = useState(false) + const [previewHtml, setPreviewHtml] = useState(null) + const [previewError, setPreviewError] = useState(null) + useEffect(() => { if (isOpen && userId && token) { fetchUserDetails() @@ -133,6 +138,22 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated } } + const loadContractPreview = async () => { + if (!userId || !token || !userDetails) return + setPreviewLoading(true) + setPreviewError(null) + try { + const html = await AdminAPI.getContractPreviewHtml(token, String(userId), userDetails.user.user_type) + setPreviewHtml(html) + } catch (e: any) { + console.error('UserDetailModal.loadContractPreview error:', e) + setPreviewError(e?.message || 'Failed to load contract preview') + setPreviewHtml(null) + } finally { + setPreviewLoading(false) + } + } + const formatDate = (dateString: string | undefined | null) => { if (!dateString) return 'N/A' return new Date(dateString).toLocaleDateString('en-US', { @@ -367,6 +388,63 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
+ {/* Contract Preview (admin verify flow) */} +
+
+

+ + Contract Preview +

+
+ + +
+
+
+ {previewError && ( +
+ {previewError} +
+ )} + {previewLoading && ( +
+ Loading preview… +
+ )} + {!previewLoading && previewHtml && ( +
+