feat(categories): add branch visibility selection
All checks were successful
Staging Build / build (push) Successful in 2m43s

This commit is contained in:
DanielS
2026-07-09 14:26:56 +02:00
parent 07c7d8c656
commit 071e669b71
4 changed files with 76 additions and 8 deletions

View File

@@ -46,6 +46,8 @@ const categorySchema = z.object({
allow_multiselect: z.boolean().default(false), allow_multiselect: z.boolean().default(false),
free_items_limit: z.coerce.number().int().min(0).default(0), free_items_limit: z.coerce.number().int().min(0).default(0),
preselect: z.boolean().default(true), preselect: z.boolean().default(true),
show_in_kauf: z.boolean().default(true),
show_in_abo: z.boolean().default(true),
}) })
type CategoryFormValues = z.infer<typeof categorySchema> type CategoryFormValues = z.infer<typeof categorySchema>
@@ -71,6 +73,8 @@ export function CategoryDialog({
allow_multiselect: category?.allow_multiselect ?? false, allow_multiselect: category?.allow_multiselect ?? false,
free_items_limit: category?.free_items_limit ?? 0, free_items_limit: category?.free_items_limit ?? 0,
preselect: category?.preselect ?? true, preselect: category?.preselect ?? true,
show_in_kauf: category?.show_in_kauf ?? true,
show_in_abo: category?.show_in_abo ?? true,
}, },
}) })
@@ -264,6 +268,54 @@ export function CategoryDialog({
)} )}
/> />
)} )}
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="show_in_kauf"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-lg border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="text-slate-900 dark:text-white font-semibold cursor-pointer">
In Kauf anzeigen
</FormLabel>
<p className="text-xs text-slate-500 dark:text-slate-400">
Im Kauf-Zweig des Wizards anzeigen.
</p>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="show_in_abo"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-lg border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="text-slate-900 dark:text-white font-semibold cursor-pointer">
In Abo anzeigen
</FormLabel>
<p className="text-xs text-slate-500 dark:text-slate-400">
Im Abo-Zweig des Wizards anzeigen.
</p>
</div>
</FormItem>
)}
/>
</div>
{form.watch('allow_multiselect') && ( {form.watch('allow_multiselect') && (
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -195,8 +195,17 @@ export function OrderWizard({
} }
}, [lastLicenseMonths, selectedBillingInterval]) }, [lastLicenseMonths, selectedBillingInterval])
const visibleCategories = useMemo(() => {
return categories.filter(cat =>
selectedBillingInterval === 'one_time'
? cat.show_in_kauf !== false
: cat.show_in_abo !== false
)
}, [categories, selectedBillingInterval])
// 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 init: Record<string, CategorySelection> = {} const init: Record<string, CategorySelection> = {}
categories.forEach(cat => { categories.forEach(cat => {
if (initialOrder) { if (initialOrder) {
@@ -211,10 +220,11 @@ export function OrderWizard({
return return
} }
} }
if (cat.allow_multiselect || cat.preselect === false) { const isVisible = initInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false
if (!isVisible || cat.allow_multiselect || cat.preselect === false) {
init[cat.id] = { productId: null, productIds: [], moduleIds: [] } init[cat.id] = { productId: null, productIds: [], moduleIds: [] }
} else { } else {
const first = products.find(p => p.category_id === cat.id && p.show_in_abo !== false) const first = products.find(p => p.category_id === cat.id && (initInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false))
init[cat.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] } init[cat.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] }
} }
}) })
@@ -223,7 +233,7 @@ export function OrderWizard({
// Only REQUIRED categories must have a product selected (which must be filtered by selected branch) // Only REQUIRED categories must have a product selected (which must be filtered by selected branch)
const allCategoriesFilled = useMemo(() => { const allCategoriesFilled = useMemo(() => {
return categories return visibleCategories
.filter(cat => cat.is_required) .filter(cat => cat.is_required)
.every(cat => { .every(cat => {
const sel = selections[cat.id] const sel = selections[cat.id]
@@ -243,7 +253,7 @@ export function OrderWizard({
? prod.show_in_kauf !== false ? prod.show_in_kauf !== false
: prod.show_in_abo !== false : prod.show_in_abo !== false
}) })
}, [categories, selections, products, selectedBillingInterval]) }, [visibleCategories, selections, products, selectedBillingInterval])
// Product validation errors (requirements and exclusions) // Product validation errors (requirements and exclusions)
const productValidationErrors = useMemo(() => { const productValidationErrors = useMemo(() => {
@@ -459,7 +469,8 @@ export function OrderWizard({
} }
const newSelections: Record<string, CategorySelection> = {} const newSelections: Record<string, CategorySelection> = {}
categories.forEach(cat => { categories.forEach(cat => {
if (cat.allow_multiselect || cat.preselect === false) { const isVisible = val === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false
if (!isVisible || cat.allow_multiselect || cat.preselect === false) {
newSelections[cat.id] = { productId: null, productIds: [], moduleIds: [] } newSelections[cat.id] = { productId: null, productIds: [], moduleIds: [] }
} else { } else {
const matchingProd = products.find(p => const matchingProd = products.find(p =>
@@ -870,7 +881,7 @@ export function OrderWizard({
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-8"> <CardContent className="space-y-8">
{categories.map((cat, idx) => { {visibleCategories.map((cat, idx) => {
const otherSelectedProductIds = Object.entries(selections) const otherSelectedProductIds = Object.entries(selections)
.filter(([cId]) => cId !== cat.id) .filter(([cId]) => cId !== cat.id)
.map(([, s]) => s.productId) .map(([, s]) => s.productId)
@@ -1122,7 +1133,7 @@ export function OrderWizard({
<CardTitle className="text-white">Zusammenfassung</CardTitle> <CardTitle className="text-white">Zusammenfassung</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{categories.map(cat => { {visibleCategories.map(cat => {
const sel = selections[cat.id] const sel = selections[cat.id]
const selectedProds: Product[] = [] const selectedProds: Product[] = []
if (cat.allow_multiselect && sel?.productIds) { if (cat.allow_multiselect && sel?.productIds) {
@@ -1311,7 +1322,7 @@ export function OrderWizard({
</CardHeader> </CardHeader>
<CardContent className="space-y-6 text-left"> <CardContent className="space-y-6 text-left">
<div className="p-4 rounded-lg bg-white/5 space-y-4"> <div className="p-4 rounded-lg bg-white/5 space-y-4">
{categories.map(cat => { {visibleCategories.map(cat => {
const sel = selections[cat.id] const sel = selections[cat.id]
const selectedProds: Product[] = [] const selectedProds: Product[] = []
if (cat.allow_multiselect && sel?.productIds) { if (cat.allow_multiselect && sel?.productIds) {

View File

@@ -29,6 +29,8 @@ export type Category = {
allow_multiselect: boolean allow_multiselect: boolean
free_items_limit: number free_items_limit: number
preselect: boolean preselect: boolean
show_in_kauf?: boolean
show_in_abo?: boolean
created_at: string created_at: string
} }

View File

@@ -0,0 +1,3 @@
-- Add show_in_kauf and show_in_abo columns to categories table
ALTER TABLE public.categories ADD COLUMN IF NOT EXISTS show_in_kauf BOOLEAN DEFAULT true NOT NULL;
ALTER TABLE public.categories ADD COLUMN IF NOT EXISTS show_in_abo BOOLEAN DEFAULT true NOT NULL;