feat: add optional categories - admins can mark categories as optional in the order wizard
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user