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:
DanielS
2026-07-23 09:45:14 +02:00
parent 086a893e19
commit 0e4495292d
7 changed files with 369 additions and 156 deletions

View File

@@ -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