From 72b906779ce95456a3e67973e0d3c583b9305f12 Mon Sep 17 00:00:00 2001 From: DanielS Date: Tue, 14 Jul 2026 12:57:46 +0200 Subject: [PATCH] feat(wizard): add license lookup panel (UI mockup) Add collapsible LicenseLookupPanel component to step 3 of the order wizard (left column). Panel queries mock LicServer data by license key and displays product, customer, dates, status badge and licensed modules. Real API endpoint can replace mock in lookupLicense(). - New: components/wizard/license-lookup-panel.tsx - Modified: components/order-wizard.tsx (4-col grid) --- shop/components/order-wizard.tsx | 15 +- .../wizard/license-lookup-panel.tsx | 476 ++++++++++++++++++ 2 files changed, 487 insertions(+), 4 deletions(-) create mode 100644 shop/components/wizard/license-lookup-panel.tsx 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. +

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