Compare commits
2 Commits
169afb6538
...
ed654f542c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed654f542c | ||
|
|
be3b0f615d |
@@ -197,7 +197,7 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button asChild className="bg-primary hover:bg-primary/95 text-white dark:text-blue-500">
|
<Button asChild className="bg-primary hover:bg-primary/95 text-white">
|
||||||
<Link href={`/auth/login?next=${pathname}`}>Anmelden</Link>
|
<Link href={`/auth/login?next=${pathname}`}>Anmelden</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -287,7 +287,7 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) {
|
|||||||
<LogOut className="w-4 h-4" /> Abmelden
|
<LogOut className="w-4 h-4" /> Abmelden
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button asChild className="w-full bg-primary hover:bg-primary/95 text-white dark:text-blue-500" onClick={() => setIsMobileMenuOpen(false)}>
|
<Button asChild className="w-full bg-primary hover:bg-primary/95 text-white" onClick={() => setIsMobileMenuOpen(false)}>
|
||||||
<Link href={`/auth/login?next=${pathname}`}>Anmelden</Link>
|
<Link href={`/auth/login?next=${pathname}`}>Anmelden</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ export function OrderWizard({
|
|||||||
const [editingIdx, setEditingIdx] = useState<number | null>(null)
|
const [editingIdx, setEditingIdx] = useState<number | null>(null)
|
||||||
const [orderNotes, setOrderNotes] = useState<string>('')
|
const [orderNotes, setOrderNotes] = useState<string>('')
|
||||||
const [toast, setToast] = useState<{ message: string; type: 'error' | 'success' } | null>(null)
|
const [toast, setToast] = useState<{ message: string; type: 'error' | 'success' } | null>(null)
|
||||||
|
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (toast) {
|
if (toast) {
|
||||||
@@ -235,6 +236,12 @@ export function OrderWizard({
|
|||||||
})
|
})
|
||||||
}, [categories, selectedBillingInterval])
|
}, [categories, selectedBillingInterval])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visibleCategories.length > 0 && !visibleCategories.some(c => c.id === activeCategoryId)) {
|
||||||
|
setActiveCategoryId(visibleCategories[0].id)
|
||||||
|
}
|
||||||
|
}, [visibleCategories, activeCategoryId])
|
||||||
|
|
||||||
// Endkunden Filterung
|
// Endkunden Filterung
|
||||||
const filteredEndCustomers = useMemo(() => {
|
const filteredEndCustomers = useMemo(() => {
|
||||||
const term = searchTerm.toLowerCase().trim()
|
const term = searchTerm.toLowerCase().trim()
|
||||||
@@ -843,8 +850,6 @@ export function OrderWizard({
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step 3: Software Selector */}
|
{/* Step 3: Software Selector */}
|
||||||
{step === 3 && (
|
{step === 3 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -852,23 +857,76 @@ export function OrderWizard({
|
|||||||
initial={{ opacity: 0, x: 20 }}
|
initial={{ opacity: 0, x: 20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
exit={{ opacity: 0, x: -20 }}
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
className="h-[calc(100vh-140px)] overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-6 gap-6 items-start">
|
<div className="grid grid-cols-1 lg:grid-cols-6 gap-6 h-full items-stretch">
|
||||||
|
|
||||||
{/* FAR LEFT: License Lookup Panel (sticky) */}
|
{/* LEFT: Categories Sidebar Menu */}
|
||||||
<div className="lg:col-span-2 sticky top-6">
|
<div className="lg:col-span-2 flex flex-col justify-start pr-4 space-y-6">
|
||||||
<LicenseLookupPanel />
|
<div>
|
||||||
</div>
|
<h1 className="text-3xl font-extrabold tracking-tight mb-2 text-gradient">
|
||||||
|
Software konfigurieren
|
||||||
{/* CENTER: Sticky stepper + scrollable categories */}
|
</h1>
|
||||||
<div className="lg:col-span-2 flex flex-col max-h-[calc(100vh-4rem)] overflow-hidden">
|
<p className="text-slate-400 text-sm">
|
||||||
{/* Sticky stepper header — only covers center column */}
|
Klicken Sie sich durch die Kategorien und wählen Sie Ihre Lizenzen.
|
||||||
<div className="shrink-0 bg-slate-950/80 backdrop-blur-md pb-4 pt-2 border-b border-white/5 mb-4">
|
</p>
|
||||||
<p className="text-center text-slate-400 text-sm mb-4">Schritt 3 von 4 — Software konfigurieren</p>
|
|
||||||
<ProgressStepper step={step} basketItemsCount={basketItems.length} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable category content area */}
|
<div className="flex flex-col gap-2">
|
||||||
|
{visibleCategories.map((cat) => {
|
||||||
|
const sel = selections[cat.id]
|
||||||
|
const hasSelection = sel?.productIds?.length > 0 || !!sel?.productId
|
||||||
|
const isRequired = cat.is_required
|
||||||
|
const isActive = activeCategoryId === cat.id
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
onClick={() => setActiveCategoryId(cat.id)}
|
||||||
|
className={`flex items-center gap-3 p-3.5 rounded-xl border transition-all duration-300 text-left relative overflow-hidden group ${
|
||||||
|
isActive
|
||||||
|
? 'bg-primary/10 border-primary text-white shadow-[0_0_20px_rgba(59,130,246,0.15)]'
|
||||||
|
: 'bg-white/5 border-white/5 text-slate-400 hover:bg-white/10 hover:border-white/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Left glow line for active category */}
|
||||||
|
{isActive && (
|
||||||
|
<div className="absolute left-0 top-0 bottom-0 w-1 bg-primary" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={`p-2 rounded-lg transition-colors ${
|
||||||
|
isActive ? 'bg-primary/20 text-primary' : 'bg-white/5 text-slate-400 group-hover:text-white'
|
||||||
|
}`}>
|
||||||
|
<CategoryIcon icon={cat.icon} className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className={`text-sm font-semibold truncate ${isActive ? 'text-white' : 'text-slate-300 group-hover:text-white'}`}>
|
||||||
|
{cat.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-slate-500 truncate mt-0.5">
|
||||||
|
{cat.description || 'Optionen ansehen'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status indicators */}
|
||||||
|
{hasSelection ? (
|
||||||
|
<div className="w-5 h-5 rounded-full bg-green-500/20 border border-green-500/35 flex items-center justify-center text-green-400 shrink-0">
|
||||||
|
<Check className="w-3 h-3" />
|
||||||
|
</div>
|
||||||
|
) : isRequired ? (
|
||||||
|
<div className="w-2 h-2 rounded-full bg-red-500 shrink-0 animate-pulse" />
|
||||||
|
) : (
|
||||||
|
<span className="text-[10px] text-slate-600 uppercase tracking-wider shrink-0 font-medium">Opt</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CENTER: Scrollable Category Selection */}
|
||||||
|
<div className="lg:col-span-2 flex flex-col h-full overflow-hidden border-x border-white/5 px-4">
|
||||||
<div className="flex-1 overflow-y-auto pr-2 subpixel-antialiased scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent">
|
<div className="flex-1 overflow-y-auto pr-2 subpixel-antialiased scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent">
|
||||||
{/* Upgrade-Modus Hinweis-Banner */}
|
{/* Upgrade-Modus Hinweis-Banner */}
|
||||||
{upgradeMode && lockedDeviceId && (
|
{upgradeMode && lockedDeviceId && (
|
||||||
@@ -895,46 +953,59 @@ export function OrderWizard({
|
|||||||
billingLabel={billingLabel}
|
billingLabel={billingLabel}
|
||||||
billingBadgeClass={billingBadgeClass}
|
billingBadgeClass={billingBadgeClass}
|
||||||
existingModuleIds={existingModuleIds}
|
existingModuleIds={existingModuleIds}
|
||||||
|
activeCategoryId={activeCategoryId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT: Summary sidebar (sticky) */}
|
{/* RIGHT: Stepper Header + Summary sidebar */}
|
||||||
<div className="lg:col-span-2 sticky top-6">
|
<div className="lg:col-span-2 flex flex-col h-full overflow-hidden pl-4">
|
||||||
<SummarySidebar
|
{/* Steps indicator at the top right */}
|
||||||
visibleCategories={visibleCategories}
|
<div className="shrink-0 pb-4 border-b border-white/5 mb-4">
|
||||||
selections={selections}
|
<p className="text-right text-slate-400 text-xs mb-2">Schritt 3 von 4 — Software konfigurieren</p>
|
||||||
products={products}
|
<ProgressStepper step={step} basketItemsCount={basketItems.length} />
|
||||||
moduleQuantities={moduleQuantities}
|
</div>
|
||||||
billingLabel={billingLabel}
|
|
||||||
oneTimeTotal={oneTimeTotal}
|
<div className="flex-1 overflow-y-auto pr-1">
|
||||||
monthlyTotal={monthlyTotal}
|
<SummarySidebar
|
||||||
oneTimeNet={oneTimeNet}
|
visibleCategories={visibleCategories}
|
||||||
oneTimeTax={oneTimeTax}
|
selections={selections}
|
||||||
oneTimeGross={oneTimeGross}
|
products={products}
|
||||||
monthlyNet={monthlyNet}
|
moduleQuantities={moduleQuantities}
|
||||||
monthlyTax={monthlyTax}
|
billingLabel={billingLabel}
|
||||||
monthlyGross={monthlyGross}
|
oneTimeTotal={oneTimeTotal}
|
||||||
updatePriceModifier={updatePriceModifier}
|
monthlyTotal={monthlyTotal}
|
||||||
allCategoriesFilled={allCategoriesFilled}
|
oneTimeNet={oneTimeNet}
|
||||||
productValidationErrors={productValidationErrors}
|
oneTimeTax={oneTimeTax}
|
||||||
basketItems={basketItems}
|
oneTimeGross={oneTimeGross}
|
||||||
editingIdx={editingIdx}
|
monthlyNet={monthlyNet}
|
||||||
editBasketItem={editBasketItem}
|
monthlyTax={monthlyTax}
|
||||||
deleteBasketItem={deleteBasketItem}
|
monthlyGross={monthlyGross}
|
||||||
deviceName={deviceName}
|
updatePriceModifier={updatePriceModifier}
|
||||||
setDeviceName={setDeviceName}
|
allCategoriesFilled={allCategoriesFilled}
|
||||||
addToBasket={addToBasket}
|
productValidationErrors={productValidationErrors}
|
||||||
isNextStepDisabled={isNextStepDisabled}
|
basketItems={basketItems}
|
||||||
hasActiveSelection={hasActiveSelection}
|
editingIdx={editingIdx}
|
||||||
nextStep={nextStep}
|
editBasketItem={editBasketItem}
|
||||||
prevStep={prevStep}
|
deleteBasketItem={deleteBasketItem}
|
||||||
/>
|
deviceName={deviceName}
|
||||||
|
setDeviceName={setDeviceName}
|
||||||
|
addToBasket={addToBasket}
|
||||||
|
isNextStepDisabled={isNextStepDisabled}
|
||||||
|
hasActiveSelection={hasActiveSelection}
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Step 4: Verification & Submit */}
|
{/* Step 4: Verification & Submit */}
|
||||||
{step === 4 && (
|
{step === 4 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -49,22 +49,47 @@ export function StepBilling({
|
|||||||
: 'border-white/5 bg-white/5 hover:border-white/20'
|
: 'border-white/5 bg-white/5 hover:border-white/20'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div>
|
<div className="flex flex-col justify-between h-full">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div>
|
||||||
<div className={`p-3 rounded-xl ${selectedBillingInterval === 'one_time' ? 'bg-primary/20 text-primary' : 'bg-white/5 text-slate-400'}`}>
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<CreditCard className="w-6 h-6" />
|
<div className={`p-3 rounded-xl ${selectedBillingInterval === 'one_time' ? 'bg-primary/20 text-primary' : 'bg-white/5 text-slate-400'}`}>
|
||||||
|
<CreditCard className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-white">Einmaliger Kauf</h3>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-bold text-white">Einmaliger Kauf</h3>
|
<p className="text-slate-400 text-sm">
|
||||||
|
Einmalige Anschaffungskosten für die Softwarelizenz. Keine monatlichen Mietgebühren.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-slate-400 text-sm">
|
|
||||||
Einmalige Anschaffungskosten für die Softwarelizenz. Keine monatlichen Mietgebühren.
|
{selectedBillingInterval === 'one_time' && (
|
||||||
</p>
|
<div className="absolute top-4 right-4 w-6 h-6 rounded-full bg-primary flex items-center justify-center text-white">
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedBillingInterval === 'one_time' && customerMode === 'select' && selectedEndCustomerId && (
|
||||||
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="mt-4 pt-4 border-t border-white/5 space-y-2 cursor-default"
|
||||||
|
>
|
||||||
|
<Label htmlFor="last-license-date" className="text-white text-xs font-medium flex items-center gap-2">
|
||||||
|
<Calendar className="w-3.5 h-3.5 text-primary" />
|
||||||
|
Datum der letzten CASPOS-Lizenz
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="last-license-date"
|
||||||
|
type="date"
|
||||||
|
value={lastLicenseDate}
|
||||||
|
onChange={e => setLastLicenseDate(e.target.value)}
|
||||||
|
className="bg-[#0b1329] border-white/10 text-white text-xs h-8 focus:border-primary"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] text-slate-500">
|
||||||
|
Ermöglicht automatische Ermittlung von Update-Gebühren.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{selectedBillingInterval === 'one_time' && (
|
|
||||||
<div className="absolute top-4 right-4 w-6 h-6 rounded-full bg-primary flex items-center justify-center text-white">
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Option 2: Abo */}
|
{/* Option 2: Abo */}
|
||||||
@@ -92,26 +117,6 @@ export function StepBilling({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/* Alte Lizenznummereingabe / Datum vorerst ausgeblendet */}
|
|
||||||
{/* {selectedBillingInterval === 'one_time' && customerMode === 'select' && selectedEndCustomerId && (
|
|
||||||
<div className="p-4 rounded-xl bg-white/5 border border-white/10 space-y-3 mt-6 max-w-md animate-in fade-in slide-in-from-top-2 duration-300">
|
|
||||||
<Label htmlFor="last-license-date" className="text-white font-medium flex items-center gap-2">
|
|
||||||
<Calendar className="w-4 h-4 text-primary" />
|
|
||||||
Datum der letzten CASPOS-Lizenz (falls vorhanden)
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="last-license-date"
|
|
||||||
type="date"
|
|
||||||
value={lastLicenseDate}
|
|
||||||
onChange={e => setLastLicenseDate(e.target.value)}
|
|
||||||
className="bg-[#0b1329] border-white/10 text-white focus:border-primary"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-slate-400">
|
|
||||||
Falls dieser Kunde bereits Lizenzen besitzt, tragen Sie das Datum der letzten Lizenzierung ein. Damit werden die korrekten Update-Gebühren ermittelt.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex justify-between border-t border-white/10 pt-6">
|
<CardFooter className="flex justify-between border-t border-white/10 pt-6">
|
||||||
<Button variant="ghost" className="text-white" onClick={prevStep}>
|
<Button variant="ghost" className="text-white" onClick={prevStep}>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { ShoppingCart, Check, AlertCircle, Lock } from 'lucide-react'
|
import { ShoppingCart, Check, AlertCircle, Lock } from 'lucide-react'
|
||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { Category, Product, CategorySelection } from '@/lib/types'
|
import { Category, Product, CategorySelection } from '@/lib/types'
|
||||||
|
|
||||||
interface StepSoftwareProps {
|
interface StepSoftwareProps {
|
||||||
@@ -27,6 +28,7 @@ interface StepSoftwareProps {
|
|||||||
billingBadgeClass: (interval: string) => string
|
billingBadgeClass: (interval: string) => string
|
||||||
/** Modul-IDs, die bereits lizenziert sind (Upgrade-Modus) */
|
/** Modul-IDs, die bereits lizenziert sind (Upgrade-Modus) */
|
||||||
existingModuleIds?: string[]
|
existingModuleIds?: string[]
|
||||||
|
activeCategoryId: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) {
|
function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) {
|
||||||
@@ -49,76 +51,85 @@ export function StepSoftware({
|
|||||||
billingLabel,
|
billingLabel,
|
||||||
billingBadgeClass,
|
billingBadgeClass,
|
||||||
existingModuleIds = [],
|
existingModuleIds = [],
|
||||||
|
activeCategoryId,
|
||||||
}: StepSoftwareProps) {
|
}: StepSoftwareProps) {
|
||||||
|
const currentCategory = visibleCategories.find(c => c.id === activeCategoryId) ?? visibleCategories[0] ?? null
|
||||||
|
|
||||||
|
if (!currentCategory) return null
|
||||||
|
|
||||||
|
const catProducts = products.filter(p => {
|
||||||
|
if (p.category_id !== currentCategory.id) return false
|
||||||
|
return selectedBillingInterval === 'one_time'
|
||||||
|
? p.show_in_kauf !== false
|
||||||
|
: p.show_in_abo !== false
|
||||||
|
})
|
||||||
|
const sel = selections[currentCategory.id]
|
||||||
|
const selectedProduct = catProducts.find(p => p.id === sel?.productId) ?? null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="glass-dark border-white/10">
|
<Card className="glass-dark border-white/10 h-full flex flex-col">
|
||||||
<CardHeader>
|
<CardHeader className="border-b border-white/5 pb-4">
|
||||||
<CardTitle className="text-2xl flex items-center gap-2 text-white">
|
<CardTitle className="text-xl flex items-center gap-2 text-white">
|
||||||
<ShoppingCart className="w-6 h-6 text-blue-400" />
|
<ShoppingCart className="w-5 h-5 text-blue-400" />
|
||||||
Software wählen
|
Optionen wählen
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-slate-300">
|
<CardDescription className="text-slate-400">
|
||||||
Wählen Sie pro Kategorie mindestens einen Artikel aus.
|
Passen Sie die Konfiguration für {currentCategory.name} an.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-8">
|
<CardContent className="flex-1 overflow-y-auto pt-6 space-y-6">
|
||||||
{visibleCategories.map((cat, idx) => {
|
<AnimatePresence mode="wait">
|
||||||
const catProducts = products.filter(p => {
|
<motion.div
|
||||||
if (p.category_id !== cat.id) return false
|
key={currentCategory.id}
|
||||||
return selectedBillingInterval === 'one_time'
|
initial={{ opacity: 0, y: 10 }}
|
||||||
? p.show_in_kauf !== false
|
animate={{ opacity: 1, y: 0 }}
|
||||||
: p.show_in_abo !== false
|
exit={{ opacity: 0, y: -10 }}
|
||||||
})
|
transition={{ duration: 0.2 }}
|
||||||
const sel = selections[cat.id]
|
className="space-y-6"
|
||||||
const selectedProduct = catProducts.find(p => p.id === sel?.productId) ?? null
|
>
|
||||||
|
{/* Category header */}
|
||||||
return (
|
<div className="flex items-center gap-3">
|
||||||
<div key={cat.id}>
|
<div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center">
|
||||||
{idx > 0 && <Separator className="bg-white/10 mb-8" />}
|
<CategoryIcon icon={currentCategory.icon} className="w-4 h-4 text-primary" />
|
||||||
|
</div>
|
||||||
{/* Category header */}
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<h3 className="font-bold text-white text-base">{currentCategory.name}</h3>
|
||||||
<div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center">
|
{currentCategory.description && (
|
||||||
<CategoryIcon icon={cat.icon} className="w-4 h-4 text-primary" />
|
<p className="text-slate-400 text-xs">{currentCategory.description}</p>
|
||||||
</div>
|
)}
|
||||||
<div>
|
</div>
|
||||||
<h3 className="font-bold text-white text-lg">{cat.name}</h3>
|
{sel?.productIds && sel.productIds.length > 0 ? (
|
||||||
{cat.description && (
|
<Badge className="ml-auto bg-green-500/20 text-green-400 border border-green-500/30">
|
||||||
<p className="text-slate-400 text-xs">{cat.description}</p>
|
<Check className="w-3 h-3 mr-1" /> {sel.productIds.length} Ausgewählt
|
||||||
)}
|
</Badge>
|
||||||
</div>
|
) : sel?.productId ? (
|
||||||
{sel?.productIds && sel.productIds.length > 0 ? (
|
<Badge className="ml-auto bg-green-500/20 text-green-400 border border-green-500/30">
|
||||||
<Badge className="ml-auto bg-green-500/20 text-green-400 border border-green-500/30">
|
<Check className="w-3 h-3 mr-1" /> Ausgewählt
|
||||||
<Check className="w-3 h-3 mr-1" /> {sel.productIds.length} Ausgewählt
|
</Badge>
|
||||||
</Badge>
|
) : currentCategory.is_required ? (
|
||||||
) : sel?.productId ? (
|
<Badge variant="destructive" className="ml-auto opacity-80">
|
||||||
<Badge className="ml-auto bg-green-500/20 text-green-400 border border-green-500/30">
|
<AlertCircle className="w-3 h-3 mr-1" /> Pflichtfeld
|
||||||
<Check className="w-3 h-3 mr-1" /> Ausgewählt
|
</Badge>
|
||||||
</Badge>
|
) : (
|
||||||
) : cat.is_required ? (
|
<Badge variant="outline" className="ml-auto border-white/20 text-slate-400">
|
||||||
<Badge variant="destructive" className="ml-auto opacity-80">
|
Optional
|
||||||
<AlertCircle className="w-3 h-3 mr-1" /> Pflichtfeld
|
</Badge>
|
||||||
</Badge>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<Badge variant="outline" className="ml-auto border-white/20 text-slate-400">
|
|
||||||
Optional
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{catProducts.length === 0 ? (
|
{catProducts.length === 0 ? (
|
||||||
<p className="text-slate-500 text-sm italic">
|
<p className="text-slate-500 text-sm italic">
|
||||||
Keine Produkte in dieser Kategorie im aktuellen Abrechnungsmodell vorhanden.
|
Keine Produkte in dieser Kategorie im aktuellen Abrechnungsmodell vorhanden.
|
||||||
</p>
|
</p>
|
||||||
) : cat.allow_multiselect ? (
|
) : currentCategory.allow_multiselect ? (
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
{catProducts.map(product => {
|
{catProducts.map(product => {
|
||||||
const isChecked = sel?.productIds?.includes(product.id) ?? false
|
const isChecked = sel?.productIds?.includes(product.id) ?? false
|
||||||
const disabled = isProductDisabled(product, cat.id)
|
const disabled = isProductDisabled(product, currentCategory.id)
|
||||||
return (
|
return (
|
||||||
<div key={product.id} className="relative">
|
<div key={product.id} className="relative">
|
||||||
<Label
|
<Label
|
||||||
onClick={() => !disabled && selectProduct(cat.id, product.id)}
|
onClick={() => !disabled && selectProduct(currentCategory.id, product.id)}
|
||||||
className={`flex flex-col items-start p-4 rounded-xl border-2 transition-all ${disabled
|
className={`flex flex-col items-start p-4 rounded-xl border-2 transition-all ${disabled
|
||||||
? 'border-white/5 bg-white/5 opacity-50 cursor-not-allowed'
|
? 'border-white/5 bg-white/5 opacity-50 cursor-not-allowed'
|
||||||
: isChecked
|
: isChecked
|
||||||
@@ -163,21 +174,21 @@ export function StepSoftware({
|
|||||||
) : (
|
) : (
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={sel?.productId ?? ''}
|
value={sel?.productId ?? ''}
|
||||||
onValueChange={id => selectProduct(cat.id, id)}
|
onValueChange={id => selectProduct(currentCategory.id, id)}
|
||||||
className="grid gap-3"
|
className="grid gap-3"
|
||||||
>
|
>
|
||||||
{catProducts.map(product => {
|
{catProducts.map(product => {
|
||||||
const disabled = isProductDisabled(product, cat.id)
|
const disabled = isProductDisabled(product, currentCategory.id)
|
||||||
return (
|
return (
|
||||||
<div key={product.id} className="relative">
|
<div key={product.id} className="relative">
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
value={product.id}
|
value={product.id}
|
||||||
id={`${cat.id}-${product.id}`}
|
id={`${currentCategory.id}-${product.id}`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="peer sr-only"
|
className="peer sr-only"
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={disabled ? undefined : `${cat.id}-${product.id}`}
|
htmlFor={disabled ? undefined : `${currentCategory.id}-${product.id}`}
|
||||||
className={`flex flex-col items-start p-4 rounded-xl border-2 border-white/5 bg-white/5 transition-all ${disabled
|
className={`flex flex-col items-start p-4 rounded-xl border-2 border-white/5 bg-white/5 transition-all ${disabled
|
||||||
? 'opacity-50 cursor-not-allowed'
|
? 'opacity-50 cursor-not-allowed'
|
||||||
: 'hover:bg-white/10 peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary/5 cursor-pointer'
|
: 'hover:bg-white/10 peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary/5 cursor-pointer'
|
||||||
@@ -234,14 +245,14 @@ export function StepSoftware({
|
|||||||
>
|
>
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={`mod-${cat.id}-${module.id}`}
|
id={`mod-${currentCategory.id}-${module.id}`}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onCheckedChange={() => !isExistingLicense && toggleModule(cat.id, module.id)}
|
onCheckedChange={() => !isExistingLicense && toggleModule(currentCategory.id, module.id)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label
|
<Label
|
||||||
htmlFor={isExistingLicense ? undefined : `mod-${cat.id}-${module.id}`}
|
htmlFor={isExistingLicense ? undefined : `mod-${currentCategory.id}-${module.id}`}
|
||||||
className={`font-medium flex justify-between text-white ${
|
className={`font-medium flex justify-between text-white ${
|
||||||
isExistingLicense ? 'cursor-default' : disabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
isExistingLicense ? 'cursor-default' : disabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
||||||
}`}
|
}`}
|
||||||
@@ -309,9 +320,8 @@ export function StepSoftware({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</motion.div>
|
||||||
)
|
</AnimatePresence>
|
||||||
})}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user