feat: change products view sorting to category grouping with subheaders
All checks were successful
Staging Build / build (push) Successful in 2m34s

This commit is contained in:
DanielS
2026-06-25 09:19:28 +02:00
parent ab9f8f93be
commit 4fa5f71d16

View File

@@ -14,12 +14,12 @@ import { Badge } from '@/components/ui/badge'
import { Edit2, Trash2, Layers, ArrowUpDown } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { deleteProduct } from '@/lib/actions/products'
import { useState, useEffect, useMemo } from 'react'
import { useState, useEffect, useMemo, Fragment } from 'react'
import { CreateProductDialog } from './create-product-dialog'
export function ProductList({ initialProducts, categories }: { initialProducts: Product[], categories: Category[] }) {
const [products, setProducts] = useState(initialProducts)
const [isSortedByCategory, setIsSortedByCategory] = useState(false)
const [isGroupedByCategory, setIsGroupedByCategory] = useState(false)
useEffect(() => {
setProducts(initialProducts)
@@ -32,60 +32,30 @@ export function ProductList({ initialProducts, categories }: { initialProducts:
}
}
const toggleSortByCategory = () => {
setIsSortedByCategory(!isSortedByCategory)
const toggleGroupByCategory = () => {
setIsGroupedByCategory(!isGroupedByCategory)
}
const displayedProducts = useMemo(() => {
if (!isSortedByCategory) return products
return [...products].sort((a, b) => {
const groupedProducts = useMemo(() => {
const groups: Record<string, { category: Category | null; products: Product[] }> = {}
products.forEach(p => {
const catId = p.category_id || 'no-category'
if (!groups[catId]) {
groups[catId] = {
category: p.category || null,
products: []
}
}
groups[catId].products.push(p)
})
return Object.values(groups).sort((a, b) => {
const orderA = a.category?.sort_order ?? 9999
const orderB = b.category?.sort_order ?? 9999
if (orderA !== orderB) return orderA - orderB
const catA = a.category?.name || ''
const catB = b.category?.name || ''
if (catA !== catB) return catA.localeCompare(catB)
return a.name.localeCompare(b.name)
return orderA - orderB
})
}, [products, isSortedByCategory])
}, [products])
return (
<Card className="bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden text-slate-900 dark:text-white">
<CardHeader className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<CardTitle>Produktübersicht</CardTitle>
<CardDescription className="text-slate-500 dark:text-slate-400">
Alle konfigurierten Software-Lösungen und Erweiterungen.
</CardDescription>
</div>
<Button
variant="outline"
size="sm"
onClick={toggleSortByCategory}
className={`border-slate-200 dark:border-white/10 hover:bg-slate-100 dark:hover:bg-white/10 text-slate-700 dark:text-white ${
isSortedByCategory ? 'bg-primary/10 border-primary text-primary hover:bg-primary/20 dark:bg-primary/20 dark:border-primary dark:text-primary-foreground' : ''
}`}
>
<ArrowUpDown className="w-4 h-4 mr-2" />
{isSortedByCategory ? 'Kategoriesortierung aktiv' : 'Nach Kategorien sortieren'}
</Button>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow className="border-slate-200 dark:border-white/10 hover:bg-slate-50 dark:hover:bg-white/5">
<TableHead className="text-slate-700 dark:text-white font-bold">Name</TableHead>
<TableHead className="text-slate-700 dark:text-white font-bold">Basispreis</TableHead>
<TableHead className="text-slate-700 dark:text-white font-bold">Module</TableHead>
<TableHead className="text-slate-700 dark:text-white font-bold">Status</TableHead>
<TableHead className="text-right text-slate-700 dark:text-white font-bold">Aktionen</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{displayedProducts.map((product) => (
const renderProductRow = (product: Product) => (
<TableRow key={product.id} className="border-slate-100 dark:border-white/5 hover:bg-slate-50 dark:hover:bg-white/5 transition-colors">
<TableCell className="font-medium text-slate-900 dark:text-white">
<div className="flex flex-col gap-1">
@@ -136,8 +106,56 @@ export function ProductList({ initialProducts, categories }: { initialProducts:
</div>
</TableCell>
</TableRow>
))}
{displayedProducts.length === 0 && (
)
return (
<Card className="bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden text-slate-900 dark:text-white">
<CardHeader className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<CardTitle>Produktübersicht</CardTitle>
<CardDescription className="text-slate-500 dark:text-slate-400">
Alle konfigurierten Software-Lösungen und Erweiterungen.
</CardDescription>
</div>
<Button
variant="outline"
size="sm"
onClick={toggleGroupByCategory}
className={`border-slate-200 dark:border-white/10 hover:bg-slate-100 dark:hover:bg-white/10 text-slate-700 dark:text-white ${
isGroupedByCategory ? 'bg-primary/10 border-primary text-primary hover:bg-primary/20 dark:bg-primary/20 dark:border-primary dark:text-primary-foreground' : ''
}`}
>
<ArrowUpDown className="w-4 h-4 mr-2" />
{isGroupedByCategory ? 'Kategoriengruppierung aktiv' : 'Nach Kategorien gruppieren'}
</Button>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow className="border-slate-200 dark:border-white/10 hover:bg-slate-50 dark:hover:bg-white/5">
<TableHead className="text-slate-700 dark:text-white font-bold">Name</TableHead>
<TableHead className="text-slate-700 dark:text-white font-bold">Basispreis</TableHead>
<TableHead className="text-slate-700 dark:text-white font-bold">Module</TableHead>
<TableHead className="text-slate-700 dark:text-white font-bold">Status</TableHead>
<TableHead className="text-right text-slate-700 dark:text-white font-bold">Aktionen</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isGroupedByCategory ? (
groupedProducts.map((group) => (
<Fragment key={group.category?.id || 'no-category'}>
<TableRow className="bg-slate-100 dark:bg-slate-800/40 hover:bg-slate-100 dark:hover:bg-slate-800/40 border-b border-slate-200 dark:border-white/10">
<TableCell colSpan={5} className="text-primary font-bold py-2.5 text-xs uppercase tracking-wider">
{group.category ? group.category.name : 'Keine Kategorie'}
</TableCell>
</TableRow>
{group.products.map((product) => renderProductRow(product))}
</Fragment>
))
) : (
products.map((product) => renderProductRow(product))
)}
{products.length === 0 && (
<TableRow>
<TableCell colSpan={5} className="text-center py-10 text-slate-500 dark:text-slate-400">
Keine Produkte gefunden. Erstellen Sie Ihr erstes Produkt!