fix(wizard): fix UI/UX and usability glitches
- constrain viewport height on wizard routes in HeaderWrapper - remove nested scrollbar in step-software - add active module glows and tooltips for locked licenses - show device name badge in step 3 card header when editing - update main action button text in summary sidebar when editing - implement double-click inline delete confirmation in step-summary - animate customer accordion and add tab-index key bindings
This commit is contained in:
@@ -17,8 +17,10 @@ export function HeaderWrapper({ navbar, children }: HeaderWrapperProps) {
|
|||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isWizard = pathname === "/order" || pathname === "/wizard";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-slate-50 text-slate-900 dark:bg-[#020617] dark:text-white">
|
<div className={`flex flex-col ${isWizard ? "h-screen overflow-hidden" : "min-h-screen"} bg-slate-50 text-slate-900 dark:bg-[#020617] dark:text-white`}>
|
||||||
{/* Demo Banner */}
|
{/* Demo Banner */}
|
||||||
<DemoWrapper>
|
<DemoWrapper>
|
||||||
<div
|
<div
|
||||||
@@ -31,7 +33,7 @@ export function HeaderWrapper({ navbar, children }: HeaderWrapperProps) {
|
|||||||
|
|
||||||
{navbar}
|
{navbar}
|
||||||
|
|
||||||
<main className="flex-1">
|
<main className={`flex-1 ${isWizard ? "overflow-hidden" : ""}`}>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { EndCustomerWithDevices, FlattenedDevice } from '@/lib/types'
|
import type { EndCustomerWithDevices, FlattenedDevice } from '@/lib/types'
|
||||||
import {
|
import {
|
||||||
@@ -250,7 +251,16 @@ export function CustomerAccordionList({ customers }: CustomerAccordionListProps)
|
|||||||
{/* Akkordeon-Header */}
|
{/* Akkordeon-Header */}
|
||||||
<div
|
<div
|
||||||
onClick={() => toggleCustomer(customer.id)}
|
onClick={() => toggleCustomer(customer.id)}
|
||||||
className="p-5 flex items-center justify-between cursor-pointer hover:bg-white/[0.02] transition-colors select-none gap-4 flex-wrap"
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
toggleCustomer(customer.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
className="p-5 flex items-center justify-between cursor-pointer hover:bg-white/[0.02] transition-colors select-none gap-4 flex-wrap focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 min-w-[240px]">
|
<div className="flex items-center gap-4 min-w-[240px]">
|
||||||
<div className="w-10 h-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary font-bold">
|
<div className="w-10 h-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary font-bold">
|
||||||
@@ -324,28 +334,38 @@ export function CustomerAccordionList({ customers }: CustomerAccordionListProps)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Ausgeklappter Bereich mit Kassen */}
|
{/* Ausgeklappter Bereich mit Kassen */}
|
||||||
{isOpen && (
|
<AnimatePresence initial={false}>
|
||||||
<div className="border-t border-white/5 bg-slate-950/40 p-5 space-y-3">
|
{isOpen && (
|
||||||
{devices.length === 0 ? (
|
<motion.div
|
||||||
<div className="text-slate-500 text-sm py-4 text-center">
|
initial={{ height: 0, opacity: 0 }}
|
||||||
Keine Kassen oder Bestellungen für diesen Kunden vorhanden.
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||||
|
className="overflow-hidden border-t border-white/5 bg-slate-950/40"
|
||||||
|
>
|
||||||
|
<div className="p-5 space-y-3">
|
||||||
|
{devices.length === 0 ? (
|
||||||
|
<div className="text-slate-500 text-sm py-4 text-center">
|
||||||
|
Keine Kassen oder Bestellungen für diesen Kunden vorhanden.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-xs uppercase tracking-wider text-slate-400 font-semibold mb-2">
|
||||||
|
Zugeordnete Kassen ({devices.length})
|
||||||
|
</p>
|
||||||
|
{devices.map((device, idx) => (
|
||||||
|
<DeviceCard
|
||||||
|
key={`${device.orderId}-${device.deviceId}-${idx}`}
|
||||||
|
device={device}
|
||||||
|
customerId={customer.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</motion.div>
|
||||||
<div className="space-y-3">
|
)}
|
||||||
<p className="text-xs uppercase tracking-wider text-slate-400 font-semibold mb-2">
|
</AnimatePresence>
|
||||||
Zugeordnete Kassen ({devices.length})
|
|
||||||
</p>
|
|
||||||
{devices.map((device, idx) => (
|
|
||||||
<DeviceCard
|
|
||||||
key={`${device.orderId}-${device.deviceId}-${idx}`}
|
|
||||||
device={device}
|
|
||||||
customerId={customer.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1085,6 +1085,7 @@ export function OrderWizard({
|
|||||||
billingBadgeClass={billingBadgeClass}
|
billingBadgeClass={billingBadgeClass}
|
||||||
existingModuleIds={existingModuleIds}
|
existingModuleIds={existingModuleIds}
|
||||||
activeCategoryId={activeCategoryId}
|
activeCategoryId={activeCategoryId}
|
||||||
|
editingDeviceName={editingIdx !== null ? (basketItems[editingIdx]?.deviceName || `Kasse ${editingIdx + 1}`) : null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ interface StepSoftwareProps {
|
|||||||
/** Modul-IDs, die bereits lizenziert sind (Upgrade-Modus) */
|
/** Modul-IDs, die bereits lizenziert sind (Upgrade-Modus) */
|
||||||
existingModuleIds?: string[]
|
existingModuleIds?: string[]
|
||||||
activeCategoryId: string | null
|
activeCategoryId: string | null
|
||||||
|
editingDeviceName?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) {
|
export function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) {
|
||||||
@@ -52,6 +53,7 @@ export function StepSoftware({
|
|||||||
billingBadgeClass,
|
billingBadgeClass,
|
||||||
existingModuleIds = [],
|
existingModuleIds = [],
|
||||||
activeCategoryId,
|
activeCategoryId,
|
||||||
|
editingDeviceName = null,
|
||||||
}: StepSoftwareProps) {
|
}: StepSoftwareProps) {
|
||||||
const currentCategory = visibleCategories.find(c => c.id === activeCategoryId) ?? visibleCategories[0] ?? null
|
const currentCategory = visibleCategories.find(c => c.id === activeCategoryId) ?? visibleCategories[0] ?? null
|
||||||
|
|
||||||
@@ -69,15 +71,22 @@ export function StepSoftware({
|
|||||||
return (
|
return (
|
||||||
<Card className="glass-dark border-white/10 h-full flex flex-col rounded-2xl">
|
<Card className="glass-dark border-white/10 h-full flex flex-col rounded-2xl">
|
||||||
<CardHeader className="border-b border-white/5 pb-4">
|
<CardHeader className="border-b border-white/5 pb-4">
|
||||||
<CardTitle className="text-xl flex items-center gap-2 text-white font-bold">
|
<CardTitle className="text-xl flex items-center justify-between gap-2 text-white font-bold flex-wrap">
|
||||||
<ShoppingCart className="w-5 h-5 text-primary" />
|
<div className="flex items-center gap-2">
|
||||||
Optionen wählen
|
<ShoppingCart className="w-5 h-5 text-primary" />
|
||||||
|
<span>Optionen wählen</span>
|
||||||
|
</div>
|
||||||
|
{editingDeviceName && (
|
||||||
|
<Badge className="bg-amber-500/20 text-amber-400 border border-amber-500/30 text-xs font-bold gap-1 px-2.5 py-1 rounded-lg animate-pulse shrink-0">
|
||||||
|
✏️ Bearbeite Kasse: "{editingDeviceName}"
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-slate-400">
|
<CardDescription className="text-slate-400">
|
||||||
Passen Sie die Konfiguration für {currentCategory.name} an.
|
Passen Sie die Konfiguration für {currentCategory.name} an.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1 overflow-y-auto pt-6 space-y-6">
|
<CardContent className="flex-1 pt-6 space-y-6">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
<motion.div
|
||||||
key={currentCategory.id}
|
key={currentCategory.id}
|
||||||
@@ -260,13 +269,14 @@ export function StepSoftware({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={module.id}
|
key={module.id}
|
||||||
|
title={isExistingLicense ? "Dieses Modul ist auf dieser Kasse bereits aktiv und dauerhaft lizenziert." : undefined}
|
||||||
className={`flex flex-col p-4 rounded-xl border transition-all duration-200 ${
|
className={`flex flex-col p-4 rounded-xl border transition-all duration-200 ${
|
||||||
isExistingLicense
|
isExistingLicense
|
||||||
? 'border-primary/20 bg-primary/5 opacity-75'
|
? 'border-primary/20 bg-primary/5 opacity-75'
|
||||||
: disabled
|
: disabled
|
||||||
? 'border-white/5 bg-white/5 opacity-40'
|
? 'border-white/5 bg-white/5 opacity-40'
|
||||||
: checked
|
: checked
|
||||||
? 'border-primary/30 bg-primary/5'
|
? 'border-primary bg-primary/10 shadow-[0_0_15px_rgba(59,130,246,0.15)] text-white'
|
||||||
: 'border-white/5 bg-white/5 hover:bg-white/10'
|
: 'border-white/5 bg-white/5 hover:bg-white/10'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ export function StepSummary({
|
|||||||
onAddNewBasketItem,
|
onAddNewBasketItem,
|
||||||
onDeleteBasketItem,
|
onDeleteBasketItem,
|
||||||
}: StepSummaryProps) {
|
}: StepSummaryProps) {
|
||||||
|
const [confirmDeleteIdx, setConfirmDeleteIdx] = React.useState<number | null>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 h-full items-stretch text-left">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 h-full items-stretch text-left">
|
||||||
{/* LINKER BEREICH: Scrollbares Bedienfeld */}
|
{/* LINKER BEREICH: Scrollbares Bedienfeld */}
|
||||||
@@ -211,16 +213,26 @@ export function StepSummary({
|
|||||||
{onDeleteBasketItem && (
|
{onDeleteBasketItem && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant={confirmDeleteIdx === itemIdx ? "destructive" : "ghost"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onDeleteBasketItem(itemIdx)
|
if (confirmDeleteIdx === itemIdx) {
|
||||||
|
onDeleteBasketItem(itemIdx)
|
||||||
|
setConfirmDeleteIdx(null)
|
||||||
|
} else {
|
||||||
|
setConfirmDeleteIdx(itemIdx)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="h-7 px-2 text-xs text-slate-400 hover:text-red-400 hover:bg-red-500/20 gap-1 rounded-lg"
|
onMouseLeave={() => setConfirmDeleteIdx(null)}
|
||||||
title="Kasse löschen"
|
className={`h-7 px-2 text-xs transition-all ${
|
||||||
|
confirmDeleteIdx === itemIdx
|
||||||
|
? 'bg-red-600 text-white hover:bg-red-700 font-bold px-3 shadow-[0_0_10px_rgba(220,38,38,0.5)]'
|
||||||
|
: 'text-slate-400 hover:text-red-400 hover:bg-red-500/20'
|
||||||
|
} gap-1 rounded-lg`}
|
||||||
>
|
>
|
||||||
<Icons.Trash2 className="w-3 h-3" /> Löschen
|
<Icons.Trash2 className="w-3.5 h-3.5" />
|
||||||
|
{confirmDeleteIdx === itemIdx ? 'Wirklich löschen?' : 'Löschen'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -283,7 +283,11 @@ export function SummarySidebar({
|
|||||||
onClick={nextStep}
|
onClick={nextStep}
|
||||||
disabled={isNextStepDisabled && basketItems.length === 0}
|
disabled={isNextStepDisabled && basketItems.length === 0}
|
||||||
>
|
>
|
||||||
Weiter zu Schritt 4 <ChevronRight className="ml-1 w-4 h-4" />
|
{editingIdx !== null ? (
|
||||||
|
<>Änderungen für "{basketItems[editingIdx]?.deviceName || `Kasse ${editingIdx + 1}`}" übernehmen</>
|
||||||
|
) : (
|
||||||
|
<>Weiter zu Schritt 4 <ChevronRight className="ml-1 w-4 h-4" /></>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" className="w-full text-slate-400 hover:text-white h-7 text-xs" onClick={prevStep}>
|
<Button variant="ghost" className="w-full text-slate-400 hover:text-white h-7 text-xs" onClick={prevStep}>
|
||||||
Zurück zum Abrechnungsmodell
|
Zurück zum Abrechnungsmodell
|
||||||
|
|||||||
Reference in New Issue
Block a user