Compare commits
2 Commits
13981970f5
...
309dc33acd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
309dc33acd | ||
|
|
d1cc6b5d5f |
@@ -31,7 +31,7 @@ graph TD
|
|||||||
- **Top-Down State-Management**:
|
- **Top-Down State-Management**:
|
||||||
- Die Wahl eines Basis-Produkts steuert die Sichtbarkeit und Wählbarkeit aller Module (Top-Down). Modul-Auswahlen dürfen niemals die Liste der wählbaren Basis-Produkte beeinflussen (Verhinderung von UI-Deadlocks).
|
- Die Wahl eines Basis-Produkts steuert die Sichtbarkeit und Wählbarkeit aller Module (Top-Down). Modul-Auswahlen dürfen niemals die Liste der wählbaren Basis-Produkte beeinflussen (Verhinderung von UI-Deadlocks).
|
||||||
- **Auto-Reset & Kaskaden-Bereinigung**: Beim Wechsel des Basis-Produkts in `selectProduct` werden inkompatible Module in anderen Kategorien automatisch über eine Kaskaden-Bereinigung (Filterung der `moduleIds` basierend auf den `exclusions` des neuen Produkts) deselektiert.
|
- **Auto-Reset & Kaskaden-Bereinigung**: Beim Wechsel des Basis-Produkts in `selectProduct` werden inkompatible Module in anderen Kategorien automatisch über eine Kaskaden-Bereinigung (Filterung der `moduleIds` basierend auf den `exclusions` des neuen Produkts) deselektiert.
|
||||||
- **Sichtbarkeits-Garantie**: Basis-Produkte werden im UI niemals durch Modulausschlüsse ausgeblendet (Vermeidung von UI-Deadlocks). Stattdessen werden inkompatible Moduloptionen im JSX deaktiviert (`disabled`).
|
- **Sichtbarkeits- & Wählbarkeits-Garantie**: Basis-Produkte werden im UI niemals durch Modulausschlüsse ausgeblendet. Nachgelagerte Produkte/Module, die mit dem ausgewählten Basis-Produkt inkompatibel sind, erhalten im JSX das `disabled`-Attribut und sind nicht anklickbar (Sperrung von "Kein Backoffice" bei "Small Business"). Klicks auf Module haben keinerlei Rückwirkung auf die Basis-Produkt-Auswahl.
|
||||||
- **Radio-Button-Verhalten (allow_multiselect = false)**: Falls eine Kategorie keine Mehrfachauswahl erlaubt, wird bei Auswahl eines neuen Moduls das zuvor ausgewählte Modul dieser Kategorie automatisch abgewählt.
|
- **Radio-Button-Verhalten (allow_multiselect = false)**: Falls eine Kategorie keine Mehrfachauswahl erlaubt, wird bei Auswahl eines neuen Moduls das zuvor ausgewählte Modul dieser Kategorie automatisch abgewählt.
|
||||||
- **Snapshot-Architektur**: Sobald eine Bestellung aufgegeben wird, werden die Kundendaten (`CustomerSnapshot`) und Produktkonfigurationen (`OrderSnapshot` samt Preisen und Modulversionen) in `orders` als JSONB-Snapshots eingefroren. Preisänderungen im Katalog haben keinen Einfluss auf bestehende Bestellungen.
|
- **Snapshot-Architektur**: Sobald eine Bestellung aufgegeben wird, werden die Kundendaten (`CustomerSnapshot`) und Produktkonfigurationen (`OrderSnapshot` samt Preisen und Modulversionen) in `orders` als JSONB-Snapshots eingefroren. Preisänderungen im Katalog haben keinen Einfluss auf bestehende Bestellungen.
|
||||||
|
|
||||||
|
|||||||
@@ -203,6 +203,19 @@ export function OrderWizard({
|
|||||||
)
|
)
|
||||||
}, [categories, selectedBillingInterval])
|
}, [categories, selectedBillingInterval])
|
||||||
|
|
||||||
|
const isProductDisabled = (product: Product, currentCategoryId: string) => {
|
||||||
|
const baseCategoryId = visibleCategories[0]?.id
|
||||||
|
if (!baseCategoryId || currentCategoryId === baseCategoryId) return false
|
||||||
|
|
||||||
|
const baseProductId = selections[baseCategoryId]?.productId
|
||||||
|
if (!baseProductId) return false
|
||||||
|
|
||||||
|
const baseProduct = products.find(p => p.id === baseProductId)
|
||||||
|
if (!baseProduct) return false
|
||||||
|
|
||||||
|
return baseProduct.exclusions?.includes(product.id) || product.exclusions?.includes(baseProductId)
|
||||||
|
}
|
||||||
|
|
||||||
// Per-category selection map: categoryId -> { productId, productIds, moduleIds }
|
// Per-category selection map: categoryId -> { productId, productIds, moduleIds }
|
||||||
const [selections, setSelections] = useState<Record<string, CategorySelection>>(() => {
|
const [selections, setSelections] = useState<Record<string, CategorySelection>>(() => {
|
||||||
const initInterval = initialOrder?.order_data?.billing_cycle ?? 'monthly'
|
const initInterval = initialOrder?.order_data?.billing_cycle ?? 'monthly'
|
||||||
@@ -970,20 +983,24 @@ export function OrderWizard({
|
|||||||
<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)
|
||||||
return (
|
return (
|
||||||
<div key={product.id} className="relative">
|
<div key={product.id} className="relative">
|
||||||
<Label
|
<Label
|
||||||
onClick={() => selectProduct(cat.id, product.id)}
|
onClick={() => !disabled && selectProduct(cat.id, product.id)}
|
||||||
className={`flex flex-col items-start p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
className={`flex flex-col items-start p-4 rounded-xl border-2 transition-all ${
|
||||||
isChecked
|
disabled
|
||||||
? 'border-primary bg-primary/5'
|
? 'border-white/5 bg-white/5 opacity-50 cursor-not-allowed'
|
||||||
: 'border-white/5 bg-white/5 hover:bg-white/10'
|
: isChecked
|
||||||
|
? 'border-primary bg-primary/5 cursor-pointer'
|
||||||
|
: 'border-white/5 bg-white/5 hover:bg-white/10 cursor-pointer'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between w-full items-center">
|
<div className="flex justify-between w-full items-center">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
|
disabled={disabled}
|
||||||
onCheckedChange={() => {}} // onClick on label handles it
|
onCheckedChange={() => {}} // onClick on label handles it
|
||||||
className="border-white/20 data-[state=checked]:bg-primary"
|
className="border-white/20 data-[state=checked]:bg-primary"
|
||||||
/>
|
/>
|
||||||
@@ -1019,16 +1036,23 @@ export function OrderWizard({
|
|||||||
onValueChange={id => selectProduct(cat.id, id)}
|
onValueChange={id => selectProduct(cat.id, id)}
|
||||||
className="grid gap-3"
|
className="grid gap-3"
|
||||||
>
|
>
|
||||||
{catProducts.map(product => (
|
{catProducts.map(product => {
|
||||||
|
const disabled = isProductDisabled(product, cat.id)
|
||||||
|
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={`${cat.id}-${product.id}`}
|
||||||
|
disabled={disabled}
|
||||||
className="peer sr-only"
|
className="peer sr-only"
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={`${cat.id}-${product.id}`}
|
htmlFor={disabled ? undefined : `${cat.id}-${product.id}`}
|
||||||
className="flex flex-col items-start p-4 rounded-xl border-2 border-white/5 bg-white/5 hover:bg-white/10 peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary/5 cursor-pointer transition-all"
|
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'
|
||||||
|
: 'hover:bg-white/10 peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary/5 cursor-pointer'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between w-full items-center">
|
<div className="flex justify-between w-full items-center">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user