feat: Add product dependencies (requirements and exclusions) to database, admin UI and wizard validation
All checks were successful
Staging Build / build (push) Successful in 2m39s

This commit is contained in:
DanielS
2026-06-25 14:38:30 +02:00
parent 5a292a8ddc
commit 47928fad3f
7 changed files with 197 additions and 4 deletions

View File

@@ -58,15 +58,29 @@ const productSchema = z.object({
billing_interval: z.enum(['one_time', 'monthly']).default('monthly'),
show_in_kauf: z.boolean().default(true),
show_in_abo: z.boolean().default(true),
requirements: z.array(z.string()).default([]),
exclusions: z.array(z.string()).default([]),
modules: z.array(moduleSchema).default([]),
})
type ProductFormValues = z.infer<typeof productSchema>
export function CreateProductDialog({ children, categories, product }: { children?: React.ReactNode, categories: Category[], product?: Product }) {
export function CreateProductDialog({
children,
categories,
product,
products = []
}: {
children?: React.ReactNode
categories: Category[]
product?: Product
products?: Product[]
}) {
const [open, setOpen] = useState(false)
const router = useRouter()
const otherProducts = products.filter(p => p.id !== product?.id)
const form = useForm<ProductFormValues>({
resolver: zodResolver(productSchema) as any,
defaultValues: {
@@ -77,6 +91,8 @@ export function CreateProductDialog({ children, categories, product }: { childre
billing_interval: product?.billing_interval || 'monthly',
show_in_kauf: product ? (product.show_in_kauf ?? true) : true,
show_in_abo: product ? (product.show_in_abo ?? true) : true,
requirements: product?.requirements || [],
exclusions: product?.exclusions || [],
modules: product?.modules?.map(m => ({
id: m.id,
name: m.name,
@@ -275,6 +291,100 @@ export function CreateProductDialog({ children, categories, product }: { childre
)}
/>
<div className="grid grid-cols-2 gap-4">
{/* Product Requirements */}
<FormField
control={form.control}
name="requirements"
render={({ field: reqField }) => (
<FormItem className="flex flex-col gap-1.5">
<FormLabel className="text-slate-900 dark:text-white font-medium">Benötigte Artikel</FormLabel>
<ScrollArea className="h-32 border border-slate-200 dark:border-white/10 rounded p-3 bg-slate-50 dark:bg-white/5">
{otherProducts.length === 0 ? (
<p className="text-xs text-slate-500 italic">Keine anderen Artikel vorhanden.</p>
) : (
<div className="space-y-2">
{otherProducts.map(other => {
const checked = (reqField.value || []).includes(other.id)
return (
<div key={other.id} className="flex items-center space-x-2">
<Checkbox
id={`prod-req-${other.id}`}
checked={checked}
onCheckedChange={(checkedState) => {
const currentVal = reqField.value || []
if (checkedState) {
reqField.onChange([...currentVal, other.id])
} else {
reqField.onChange(currentVal.filter(id => id !== other.id))
}
}}
/>
<label
htmlFor={`prod-req-${other.id}`}
className="text-xs text-slate-700 dark:text-slate-300 cursor-pointer select-none truncate block"
title={other.name}
>
{other.name}
</label>
</div>
)
})}
</div>
)}
</ScrollArea>
<FormMessage />
</FormItem>
)}
/>
{/* Product Exclusions */}
<FormField
control={form.control}
name="exclusions"
render={({ field: exclField }) => (
<FormItem className="flex flex-col gap-1.5">
<FormLabel className="text-slate-900 dark:text-white font-medium">Schließt aus</FormLabel>
<ScrollArea className="h-32 border border-slate-200 dark:border-white/10 rounded p-3 bg-slate-50 dark:bg-white/5">
{otherProducts.length === 0 ? (
<p className="text-xs text-slate-500 italic">Keine anderen Artikel vorhanden.</p>
) : (
<div className="space-y-2">
{otherProducts.map(other => {
const checked = (exclField.value || []).includes(other.id)
return (
<div key={other.id} className="flex items-center space-x-2">
<Checkbox
id={`prod-excl-${other.id}`}
checked={checked}
onCheckedChange={(checkedState) => {
const currentVal = exclField.value || []
if (checkedState) {
exclField.onChange([...currentVal, other.id])
} else {
exclField.onChange(currentVal.filter(id => id !== other.id))
}
}}
/>
<label
htmlFor={`prod-excl-${other.id}`}
className="text-xs text-slate-700 dark:text-slate-300 cursor-pointer select-none truncate block"
title={other.name}
>
{other.name}
</label>
</div>
)
})}
</div>
)}
</ScrollArea>
<FormMessage />
</FormItem>
)}
/>
</div>
<Separator className="bg-slate-200 dark:bg-white/10" />
<div className="space-y-4">