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
All checks were successful
Staging Build / build (push) Successful in 2m39s
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -154,7 +154,7 @@ export function ProductList({ initialProducts, categories }: { initialProducts:
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
<CreateProductDialog categories={categories} product={product}>
|
||||
<CreateProductDialog categories={categories} product={product} products={products}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white">
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user