diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index c557564..0d7b39d 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -16,6 +16,7 @@ import { StepBilling } from './wizard/step-billing' import { StepSoftware } from './wizard/step-software' import { StepSummary } from './wizard/step-summary' import { SummarySidebar } from './wizard/summary-sidebar' +import { LicenseLookupPanel } from './wizard/license-lookup-panel' type CategorySelection = { productId: string | null // selected product in this category @@ -734,10 +735,16 @@ export function OrderWizard({ animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} > -
- {/* LEFT: Sticky header (stepper) + scrollable categories */} +
+ + {/* FAR LEFT: License Lookup Panel (sticky) */} +
+ +
+ + {/* CENTER: Sticky stepper + scrollable categories */}
- {/* Sticky stepper header — only covers left column */} + {/* Sticky stepper header — only covers center column */}

Schritt 3 von 4 — Software konfigurieren

@@ -760,7 +767,7 @@ export function OrderWizard({ />
- {/* RIGHT: Independent full-height sidebar column */} + {/* RIGHT: Summary sidebar (sticky) */}
= { + 'CAS-2023-PRO-00123': { + licenseKey: 'CAS-2023-PRO-00123', + status: 'active', + product: 'CAS genesisWorld Professional', + edition: 'Professional', + version: '14.1.2', + seats: 10, + customer: 'Mustermann GmbH', + contact: 'Max Mustermann', + issuedAt: '2023-04-01', + expiresAt: '2026-03-31', + maintenanceUntil: '2025-03-31', + modules: ['CRM', 'ERP-Link', 'Mobile Client', 'Web Client'], + }, + 'CAS-2020-STD-00456': { + licenseKey: 'CAS-2020-STD-00456', + status: 'expired', + product: 'CAS genesisWorld Standard', + edition: 'Standard', + version: '12.0.0', + seats: 5, + customer: 'Beispiel AG', + contact: 'Erika Muster', + issuedAt: '2020-01-15', + expiresAt: '2023-01-14', + maintenanceUntil: '2022-01-14', + modules: ['CRM'], + }, + 'CAS-2024-ENT-00789': { + licenseKey: 'CAS-2024-ENT-00789', + status: 'active', + product: 'CAS genesisWorld Enterprise', + edition: 'Enterprise', + version: '15.0.0', + seats: 50, + customer: 'Tech Solutions GmbH & Co. KG', + contact: 'Julia Schneider', + issuedAt: '2024-01-01', + expiresAt: '2026-12-31', + maintenanceUntil: '2026-12-31', + modules: ['CRM', 'ERP-Link', 'Mobile Client', 'Web Client', 'AI Assistant', 'Analytics Pro'], + }, +} + +// ─── TYPES ─────────────────────────────────────────────────────────────────── +export type LicenseStatus = 'active' | 'expired' | 'invalid' | 'suspended' + +export interface LicenseInfo { + licenseKey: string + status: LicenseStatus + product: string + edition: string + version: string + seats: number + customer: string + contact: string + issuedAt: string + expiresAt: string + maintenanceUntil: string + modules: string[] +} + +interface LicenseLookupPanelProps { + /** Called when a valid license is found – so the wizard can prefill fields */ + onLicenseResolved?: (info: LicenseInfo) => void +} + +// ─── HELPERS ───────────────────────────────────────────────────────────────── +function statusConfig(status: LicenseStatus) { + switch (status) { + case 'active': + return { + label: 'Aktiv', + icon: , + cls: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40', + ring: 'border-emerald-500/30', + glow: 'shadow-emerald-500/10', + } + case 'expired': + return { + label: 'Abgelaufen', + icon: , + cls: 'bg-amber-500/20 text-amber-300 border-amber-500/40', + ring: 'border-amber-500/30', + glow: 'shadow-amber-500/10', + } + case 'suspended': + return { + label: 'Gesperrt', + icon: , + cls: 'bg-red-500/20 text-red-300 border-red-500/40', + ring: 'border-red-500/30', + glow: 'shadow-red-500/10', + } + default: + return { + label: 'Ungültig', + icon: , + cls: 'bg-red-500/20 text-red-300 border-red-500/40', + ring: 'border-red-500/30', + glow: 'shadow-red-500/10', + } + } +} + +function fmt(dateStr: string) { + if (!dateStr) return '—' + return new Date(dateStr).toLocaleDateString('de-DE', { + day: '2-digit', month: '2-digit', year: 'numeric', + }) +} + +// ─── MOCK LOOKUP (replace with real LicServer API call) ────────────────────── +async function lookupLicense(key: string): Promise { + // TODO: replace with → fetch(`/api/license-lookup?key=${encodeURIComponent(key)}`) + await new Promise(r => setTimeout(r, 800)) // simulate network latency + return MOCK_LICENSES[key.trim().toUpperCase()] ?? null +} + +// ─── MAIN COMPONENT ─────────────────────────────────────────────────────────── +export function LicenseLookupPanel({ onLicenseResolved }: LicenseLookupPanelProps) { + const [licenseKey, setLicenseKey] = useState('') + const [result, setResult] = useState(null) + const [notFound, setNotFound] = useState(false) + const [isPending, startTransition] = useTransition() + const [expanded, setExpanded] = useState(true) + const [modulesOpen, setModulesOpen] = useState(false) + const inputRef = useRef(null) + + const handleSearch = () => { + if (!licenseKey.trim()) return + setResult(null) + setNotFound(false) + startTransition(async () => { + const info = await lookupLicense(licenseKey) + if (info) { + setResult(info) + onLicenseResolved?.(info) + } else { + setNotFound(true) + } + }) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') handleSearch() + } + + const handleClear = () => { + setLicenseKey('') + setResult(null) + setNotFound(false) + inputRef.current?.focus() + } + + const sc = result ? statusConfig(result.status) : null + + return ( +
+ + {/* ── Panel Header (toggle) ─────────────────────────────────────────── */} + + + {/* ── Collapsible Body ──────────────────────────────────────────────── */} + + {expanded && ( + +
+ + {/* ── Input + Search Button ──────────────────────────────── */} +
+

+ Geben Sie Ihre bisherige CAS-Lizenznummer ein, + um Lizenzinformationen automatisch abzurufen. +

+ +
+ {/* Input */} +
+ + { + setLicenseKey(e.target.value) + setResult(null) + setNotFound(false) + }} + onKeyDown={handleKeyDown} + placeholder="z. B. CAS-2023-PRO-00123" + className="pl-9 pr-8 bg-white/5 border-white/10 text-white placeholder:text-slate-500 + focus:border-violet-500/60 focus:ring-violet-500/20 rounded-xl text-sm h-10" + /> + {licenseKey && ( + + )} +
+ + {/* Search Button */} + +
+ + {/* Demo quick-fill hints */} +

+ Demo:  + setLicenseKey('CAS-2023-PRO-00123')} + > + CAS-2023-PRO-00123 + + {' · '} + setLicenseKey('CAS-2020-STD-00456')} + > + CAS-2020-STD-00456 + +

+
+ + {/* ── Animated Result Area ──────────────────────────────── */} + + + {/* Loading skeleton */} + {isPending && ( + + {[1, 2, 3].map(i => ( +
+ ))} +

+ Lizenzserver wird abgefragt… +

+ + )} + + {/* Not Found */} + {notFound && !isPending && ( + + +
+

Lizenz nicht gefunden

+

+ Die eingegebene Nummer wurde im Lizenzsystem nicht gefunden. + Bitte prüfen Sie die Eingabe. +

+
+
+ )} + + {/* Result Card */} + {result && !isPending && sc && ( + + {/* Status Bar */} +
+
+ + + {result.licenseKey} + +
+ + {sc.icon} + {sc.label} + +
+ + {/* Info Rows */} +
+ + {/* Product */} +
+ +
+

Produkt

+

{result.product}

+

+ {result.edition} · v{result.version} · {result.seats} Arbeitsplätze +

+
+
+ + {/* Customer */} +
+ +
+

Lizenznehmer

+

{result.customer}

+

+ + {result.contact} +

+
+
+ + {/* Date Grid */} +
+
+

+ Ausgestellt +

+

{fmt(result.issuedAt)}

+
+
+

+ Läuft ab +

+

+ {fmt(result.expiresAt)} +

+
+
+

+ Wartung bis +

+

{fmt(result.maintenanceUntil)}

+
+
+ + {/* Modules (collapsible) */} + {result.modules.length > 0 && ( +
+ + + {modulesOpen && ( + +
+ {result.modules.map(m => ( + + {m} + + ))} +
+
+ )} +
+
+ )} +
+ + {/* Bottom hint */} + {result.status === 'active' && ( +
+

+ + Lizenz erkannt — Daten können für die Bestellung übernommen werden. +

+
+ )} + {result.status === 'expired' && ( +
+

+ + Lizenz abgelaufen — Upgrade empfohlen. +

+
+ )} +
+ )} + + +
+
+ )} +
+
+ ) +}