feat(my-customers): flatten orders into per-device cards
- split orders by device_name into FlattenedDevice entries - strip device prefixes from product and module names - render one DeviceCard per Kasse in the customer accordion - add targeted upgrade URL with orderId, customer_id, device_id - wizard starts at step 3 in upgrade mode with locked customer - mark existing licensed modules as read-only with badge
This commit is contained in:
@@ -8,7 +8,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ShoppingCart, Check, AlertCircle } from 'lucide-react'
|
||||
import { ShoppingCart, Check, AlertCircle, Lock } from 'lucide-react'
|
||||
import * as Icons from 'lucide-react'
|
||||
import { Category, Product, CategorySelection } from '@/lib/types'
|
||||
|
||||
@@ -25,6 +25,8 @@ interface StepSoftwareProps {
|
||||
selectedBillingInterval: 'one_time' | 'monthly'
|
||||
billingLabel: (interval: string) => string
|
||||
billingBadgeClass: (interval: string) => string
|
||||
/** Modul-IDs, die bereits lizenziert sind (Upgrade-Modus) */
|
||||
existingModuleIds?: string[]
|
||||
}
|
||||
|
||||
function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) {
|
||||
@@ -46,6 +48,7 @@ export function StepSoftware({
|
||||
selectedBillingInterval,
|
||||
billingLabel,
|
||||
billingBadgeClass,
|
||||
existingModuleIds = [],
|
||||
}: StepSoftwareProps) {
|
||||
return (
|
||||
<Card className="glass-dark border-white/10">
|
||||
@@ -215,37 +218,57 @@ export function StepSoftware({
|
||||
<div className="mt-4 space-y-3 pl-2 border-l-2 border-primary/30">
|
||||
<p className="text-sm font-semibold text-white ml-2">Zusatzmodule:</p>
|
||||
{selectedProduct.modules.map(module => {
|
||||
const disabled = isModuleDisabled(module, sel?.moduleIds ?? [])
|
||||
const checked = sel?.moduleIds.includes(module.id) ?? false
|
||||
const isExistingLicense = existingModuleIds.includes(module.id)
|
||||
const disabled = isExistingLicense || isModuleDisabled(module, sel?.moduleIds ?? [])
|
||||
const checked = isExistingLicense || (sel?.moduleIds.includes(module.id) ?? false)
|
||||
return (
|
||||
<div
|
||||
key={module.id}
|
||||
className={`flex flex-col p-3 rounded-lg border border-white/5 bg-white/5 ml-2 transition-colors ${disabled ? 'opacity-50' : 'hover:bg-white/10'}`}
|
||||
className={`flex flex-col p-3 rounded-lg border ml-2 transition-colors ${
|
||||
isExistingLicense
|
||||
? 'border-primary/20 bg-primary/5 opacity-75'
|
||||
: disabled
|
||||
? 'border-white/5 bg-white/5 opacity-50'
|
||||
: 'border-white/5 bg-white/5 hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id={`mod-${cat.id}-${module.id}`}
|
||||
checked={checked}
|
||||
onCheckedChange={() => toggleModule(cat.id, module.id)}
|
||||
onCheckedChange={() => !isExistingLicense && toggleModule(cat.id, module.id)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<Label
|
||||
htmlFor={`mod-${cat.id}-${module.id}`}
|
||||
className={`font-medium cursor-pointer flex justify-between text-white ${disabled ? 'cursor-not-allowed' : ''}`}
|
||||
htmlFor={isExistingLicense ? undefined : `mod-${cat.id}-${module.id}`}
|
||||
className={`font-medium flex justify-between text-white ${
|
||||
isExistingLicense ? 'cursor-default' : disabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
<span>{module.name}</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
{module.name}
|
||||
{isExistingLicense && (
|
||||
<span className="inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded bg-primary/20 text-primary border border-primary/30 font-semibold">
|
||||
<Lock className="w-2.5 h-2.5" /> Bereits lizenziert
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-primary font-bold">
|
||||
+{new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
}).format(module.price)}
|
||||
{isExistingLicense ? (
|
||||
<span className="text-slate-500 text-xs">inkl.</span>
|
||||
) : (
|
||||
<>+{new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
}).format(module.price)}</>
|
||||
)}
|
||||
</span>
|
||||
</Label>
|
||||
{module.description && (
|
||||
<p className="text-xs text-slate-400">{module.description}</p>
|
||||
)}
|
||||
{disabled && (
|
||||
{!isExistingLicense && disabled && (
|
||||
<p className="text-[10px] text-destructive mt-1">
|
||||
{module.requirements?.length && !module.requirements.some(
|
||||
reqId => sel?.moduleIds.includes(reqId)
|
||||
@@ -258,7 +281,7 @@ export function StepSoftware({
|
||||
</div>
|
||||
|
||||
{/* Scalable Quantity */}
|
||||
{checked && module.has_quantity && (
|
||||
{checked && !isExistingLicense && module.has_quantity && (
|
||||
<div className="flex items-center gap-3 mt-3 pl-8 pt-2 border-t border-white/5">
|
||||
<Label htmlFor={`qty-${module.id}`} className="text-xs text-slate-400">Menge:</Label>
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user