feat: add optional categories - admins can mark categories as optional in the order wizard

This commit is contained in:
DanielS
2026-05-01 02:29:00 +02:00
parent cb6668d614
commit 1ec164cd7b
5 changed files with 55 additions and 7 deletions

View File

@@ -23,6 +23,7 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { createCategory, updateCategory } from '@/lib/actions/products' import { createCategory, updateCategory } from '@/lib/actions/products'
import { Category } from '@/lib/types' import { Category } from '@/lib/types'
import { import {
@@ -40,6 +41,7 @@ const categorySchema = z.object({
description: z.string().optional(), description: z.string().optional(),
icon: z.string().default('Package'), icon: z.string().default('Package'),
sort_order: z.coerce.number().int().min(0).default(0), sort_order: z.coerce.number().int().min(0).default(0),
is_required: z.boolean().default(true),
}) })
type CategoryFormValues = z.infer<typeof categorySchema> type CategoryFormValues = z.infer<typeof categorySchema>
@@ -60,6 +62,7 @@ export function CategoryDialog({
description: category?.description || '', description: category?.description || '',
icon: category?.icon || 'Package', icon: category?.icon || 'Package',
sort_order: category?.sort_order ?? 0, sort_order: category?.sort_order ?? 0,
is_required: category?.is_required ?? true,
}, },
}) })
@@ -178,6 +181,29 @@ export function CategoryDialog({
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="is_required"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-lg border border-white/10 bg-white/5 p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="text-white font-semibold cursor-pointer">
Pflichtauswahl im Bestellvorgang
</FormLabel>
<p className="text-xs text-slate-400">
Wenn aktiv, muss der Kunde aus dieser Kategorie einen Artikel wählen.
Wenn deaktiviert, ist die Kategorie optional.
</p>
</div>
</FormItem>
)}
/>
</div> </div>
</ScrollArea> </ScrollArea>
<DialogFooter className="pt-4"> <DialogFooter className="pt-4">

View File

@@ -12,6 +12,7 @@ import {
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Edit2, Trash2, Cloud, Utensils, HardDrive, Package, ArrowUpDown } from 'lucide-react' import { Edit2, Trash2, Cloud, Utensils, HardDrive, Package, ArrowUpDown } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { deleteCategory } from '@/lib/actions/products' import { deleteCategory } from '@/lib/actions/products'
import { useState } from 'react' import { useState } from 'react'
import { CategoryDialog } from './category-dialog' import { CategoryDialog } from './category-dialog'
@@ -55,6 +56,7 @@ export function CategoryList({ initialCategories }: { initialCategories: Categor
<ArrowUpDown className="w-3 h-3" /> Reihenfolge <ArrowUpDown className="w-3 h-3" /> Reihenfolge
</span> </span>
</TableHead> </TableHead>
<TableHead className="text-white font-bold w-24">Typ</TableHead>
<TableHead className="text-right text-white font-bold">Aktionen</TableHead> <TableHead className="text-right text-white font-bold">Aktionen</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -73,6 +75,13 @@ export function CategoryList({ initialCategories }: { initialCategories: Categor
{category.sort_order} {category.sort_order}
</span> </span>
</TableCell> </TableCell>
<TableCell>
{category.is_required ? (
<Badge className="bg-green-500/20 text-green-400 border border-green-500/30 whitespace-nowrap">Pflicht</Badge>
) : (
<Badge variant="outline" className="border-white/20 text-slate-400 whitespace-nowrap">Optional</Badge>
)}
</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<CategoryDialog category={category}> <CategoryDialog category={category}>
@@ -94,7 +103,7 @@ export function CategoryList({ initialCategories }: { initialCategories: Categor
))} ))}
{categories.length === 0 && ( {categories.length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-center py-10 text-slate-400"> <TableCell colSpan={6} className="text-center py-10 text-slate-400">
Keine Kategorien gefunden. Keine Kategorien gefunden.
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -88,9 +88,9 @@ export function OrderWizard({
return init return init
}) })
// All categories must have a product selected // Only REQUIRED categories must have a product selected
const allCategoriesFilled = useMemo( const allCategoriesFilled = useMemo(
() => categories.every(cat => !!selections[cat.id]?.productId), () => categories.filter(cat => cat.is_required).every(cat => !!selections[cat.id]?.productId),
[categories, selections] [categories, selections]
) )
@@ -234,10 +234,14 @@ export function OrderWizard({
<Badge className="ml-auto bg-green-500/20 text-green-400 border border-green-500/30"> <Badge className="ml-auto bg-green-500/20 text-green-400 border border-green-500/30">
<Check className="w-3 h-3 mr-1" /> Ausgewählt <Check className="w-3 h-3 mr-1" /> Ausgewählt
</Badge> </Badge>
) : ( ) : cat.is_required ? (
<Badge variant="destructive" className="ml-auto opacity-80"> <Badge variant="destructive" className="ml-auto opacity-80">
<AlertCircle className="w-3 h-3 mr-1" /> Pflichtfeld <AlertCircle className="w-3 h-3 mr-1" /> Pflichtfeld
</Badge> </Badge>
) : (
<Badge variant="outline" className="ml-auto border-white/20 text-slate-400">
Optional
</Badge>
)} )}
</div> </div>
@@ -353,8 +357,12 @@ export function OrderWizard({
const prod = products.find(p => p.id === sel?.productId) const prod = products.find(p => p.id === sel?.productId)
if (!prod) return ( if (!prod) return (
<div key={cat.id} className="text-xs text-slate-500 italic flex items-center gap-1"> <div key={cat.id} className="text-xs text-slate-500 italic flex items-center gap-1">
{cat.is_required ? (
<AlertCircle className="w-3 h-3 text-destructive" /> <AlertCircle className="w-3 h-3 text-destructive" />
{cat.name}: nicht gewählt ) : (
<span className="w-3 h-3 inline-block" />
)}
{cat.name}: {cat.is_required ? 'nicht gewählt' : 'nicht gewählt (optional)'}
</div> </div>
) )
return ( return (
@@ -392,7 +400,7 @@ export function OrderWizard({
{!allCategoriesFilled && ( {!allCategoriesFilled && (
<p className="text-xs text-destructive flex items-center gap-1"> <p className="text-xs text-destructive flex items-center gap-1">
<AlertCircle className="w-3 h-3" /> <AlertCircle className="w-3 h-3" />
Bitte aus jeder Kategorie einen Artikel wählen. Bitte aus jeder Pflichtkategorie einen Artikel wählen.
</p> </p>
)} )}
</CardContent> </CardContent>

View File

@@ -17,6 +17,7 @@ export type Category = {
description?: string | null; description?: string | null;
icon?: string | null; icon?: string | null;
sort_order: number; sort_order: number;
is_required: boolean;
created_at: string; created_at: string;
}; };

View File

@@ -0,0 +1,4 @@
-- Add is_required column to categories
-- When true (default), a product must be selected from this category in the order wizard
-- When false, the category is optional and can be skipped
ALTER TABLE public.categories ADD COLUMN IF NOT EXISTS is_required BOOLEAN DEFAULT true NOT NULL;