diff --git a/shop/components/admin/product-list.tsx b/shop/components/admin/product-list.tsx index 356dfe8..3f5facf 100644 --- a/shop/components/admin/product-list.tsx +++ b/shop/components/admin/product-list.tsx @@ -11,18 +11,57 @@ import { } from '@/components/ui/table' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { Edit2, Trash2, Layers, ArrowUpDown } from 'lucide-react' +import { Edit2, Trash2, Layers, ArrowUpDown, Copy } from 'lucide-react' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' -import { deleteProduct } from '@/lib/actions/products' +import { deleteProduct, transferModules } from '@/lib/actions/products' import { useState, useEffect, useMemo, Fragment } from 'react' import { CreateProductDialog } from './create-product-dialog' +import { useRouter } from 'next/navigation' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Label } from "@/components/ui/label" export function ProductList({ initialProducts, categories }: { initialProducts: Product[], categories: Category[] }) { const [products, setProducts] = useState(initialProducts) const [isGroupedByCategory, setIsGroupedByCategory] = useState(false) const [showKauf, setShowKauf] = useState(true) const [showAbo, setShowAbo] = useState(true) + const [copySourceProduct, setCopySourceProduct] = useState(null) + const [copyTargetProductId, setCopyTargetProductId] = useState('') + const [copyMode, setCopyMode] = useState<'overwrite' | 'append'>('overwrite') + const [isCopying, setIsCopying] = useState(false) + const router = useRouter() + + const handleCopyModules = async () => { + if (!copySourceProduct || !copyTargetProductId) return + setIsCopying(true) + try { + await transferModules(copySourceProduct.id, copyTargetProductId, copyMode) + setCopySourceProduct(null) + setCopyTargetProductId('') + router.refresh() + } catch (err) { + console.error('Error copying modules:', err) + alert('Fehler beim Kopieren der Module.') + } finally { + setIsCopying(false) + } + } useEffect(() => { setProducts(initialProducts) @@ -103,6 +142,18 @@ export function ProductList({ initialProducts, categories }: { initialProducts:
+ -
- - - - - - Name - Basispreis - Module - Status - Aktionen - - - - {isGroupedByCategory ? ( - groupedProducts.map((group) => ( - - - - {group.category ? group.category.name : 'Keine Kategorie'} - - - {group.products.map((product) => renderProductRow(product))} - - )) - ) : ( - filteredProducts.map((product) => renderProductRow(product)) - )} - {filteredProducts.length === 0 && ( - - - Keine Produkte gefunden. Passen Sie Ihre Filter an. - + <> + + +
+ Produktübersicht + + Alle konfigurierten Software-Lösungen und Erweiterungen. + +
+
+ + + +
+
+ +
+ + + Name + Basispreis + Module + Status + Aktionen - )} - -
-
- + + + {isGroupedByCategory ? ( + groupedProducts.map((group) => ( + + + + {group.category ? group.category.name : 'Keine Kategorie'} + + + {group.products.map((product) => renderProductRow(product))} + + )) + ) : ( + filteredProducts.map((product) => renderProductRow(product)) + )} + {filteredProducts.length === 0 && ( + + + Keine Produkte gefunden. Passen Sie Ihre Filter an. + + + )} + + + + + + { if (!open) setCopySourceProduct(null) }}> + + + Module übertragen + + Kopieren Sie alle Module von {copySourceProduct?.name} auf ein anderes Produkt. + + + +
+
+ + +
+ +
+ + setCopyMode(val)} className="flex flex-col gap-2"> +
+ + +
+
+ + +
+
+
+
+ + + + + +
+
+ ) } diff --git a/shop/lib/actions/products.ts b/shop/lib/actions/products.ts index c063e71..732bb96 100644 --- a/shop/lib/actions/products.ts +++ b/shop/lib/actions/products.ts @@ -148,3 +148,61 @@ export async function deleteProduct(id: string) { revalidatePath('/admin/products') revalidatePath('/order') } + +export async function transferModules( + sourceProductId: string, + targetProductId: string, + mode: 'overwrite' | 'append' +) { + const supabase = await createClient() + + // 1. Fetch modules of source product + const { data: sourceModules, error: fetchError } = await supabase + .from('product_modules') + .select('*') + .eq('product_id', sourceProductId) + + if (fetchError) throw fetchError + if (!sourceModules || sourceModules.length === 0) return + + // 2. If overwrite, delete existing modules of target product + if (mode === 'overwrite') { + const { error: deleteError } = await supabase + .from('product_modules') + .delete() + .eq('product_id', targetProductId) + if (deleteError) throw deleteError + } + + // 3. Map IDs to new UUIDs and fix requirements/exclusions + const idMap: Record = {} + sourceModules.forEach(m => { + idMap[m.id] = randomUUID() + }) + + const newModules = sourceModules.map(m => { + const newId = idMap[m.id] + const requirements = (m.requirements || []).map((id: string) => idMap[id] || id) + const exclusions = (m.exclusions || []).map((id: string) => idMap[id] || id) + return { + id: newId, + product_id: targetProductId, + name: m.name, + description: m.description, + price: m.price, + requirements, + exclusions, + has_quantity: m.has_quantity ?? false, + } + }) + + // 4. Insert into DB + const { error: insertError } = await supabase + .from('product_modules') + .insert(newModules) + + if (insertError) throw insertError + + revalidatePath('/admin/products') + revalidatePath('/order') +}