feat: Add action and dialog to copy/transfer modules between products
All checks were successful
Staging Build / build (push) Successful in 2m39s

This commit is contained in:
DanielS
2026-06-25 14:03:26 +02:00
parent 9246fb7d77
commit 5a292a8ddc
2 changed files with 244 additions and 74 deletions

View File

@@ -11,18 +11,57 @@ import {
} from '@/components/ui/table' } from '@/components/ui/table'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' 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 { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' 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 { useState, useEffect, useMemo, Fragment } from 'react'
import { CreateProductDialog } from './create-product-dialog' 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[] }) { export function ProductList({ initialProducts, categories }: { initialProducts: Product[], categories: Category[] }) {
const [products, setProducts] = useState(initialProducts) const [products, setProducts] = useState(initialProducts)
const [isGroupedByCategory, setIsGroupedByCategory] = useState(false) const [isGroupedByCategory, setIsGroupedByCategory] = useState(false)
const [showKauf, setShowKauf] = useState(true) const [showKauf, setShowKauf] = useState(true)
const [showAbo, setShowAbo] = useState(true) const [showAbo, setShowAbo] = useState(true)
const [copySourceProduct, setCopySourceProduct] = useState<Product | null>(null)
const [copyTargetProductId, setCopyTargetProductId] = useState<string>('')
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(() => { useEffect(() => {
setProducts(initialProducts) setProducts(initialProducts)
@@ -103,6 +142,18 @@ export function ProductList({ initialProducts, categories }: { initialProducts:
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<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"
onClick={() => {
setCopySourceProduct(product)
setCopyTargetProductId('')
}}
title="Module kopieren"
>
<Copy className="h-4 w-4" />
</Button>
<CreateProductDialog categories={categories} product={product}> <CreateProductDialog categories={categories} product={product}>
<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"> <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" /> <Edit2 className="h-4 w-4" />
@@ -122,6 +173,7 @@ export function ProductList({ initialProducts, categories }: { initialProducts:
) )
return ( 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"> <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 lg:flex-row lg:items-center lg:justify-between gap-4"> <CardHeader className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div> <div>
@@ -195,5 +247,65 @@ export function ProductList({ initialProducts, categories }: { initialProducts:
</Table> </Table>
</CardContent> </CardContent>
</Card> </Card>
<Dialog open={!!copySourceProduct} onOpenChange={(open) => { if (!open) setCopySourceProduct(null) }}>
<DialogContent className="sm:max-w-[425px] bg-white dark:bg-slate-950 text-slate-900 dark:text-white border-slate-200 dark:border-white/10 shadow-2xl">
<DialogHeader>
<DialogTitle>Module übertragen</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400">
Kopieren Sie alle Module von <strong>{copySourceProduct?.name}</strong> auf ein anderes Produkt.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="flex flex-col gap-2">
<Label htmlFor="target-product">Ziel-Produkt</Label>
<Select value={copyTargetProductId} onValueChange={setCopyTargetProductId}>
<SelectTrigger id="target-product" className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10 text-slate-900 dark:text-white">
<SelectValue placeholder="Ziel-Produkt auswählen" />
</SelectTrigger>
<SelectContent className="bg-white dark:bg-slate-950 border-slate-200 dark:border-white/10 text-slate-900 dark:text-white">
{products
.filter(p => p.id !== copySourceProduct?.id)
.map(p => (
<SelectItem key={p.id} value={p.id} className="focus:bg-slate-100 dark:focus:bg-white/5">
{p.name}
</SelectItem>
))
}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label>Übertragungsmodus</Label>
<RadioGroup value={copyMode} onValueChange={(val: any) => setCopyMode(val)} className="flex flex-col gap-2">
<div className="flex items-center space-x-2">
<RadioGroupItem value="overwrite" id="mode-overwrite" />
<Label htmlFor="mode-overwrite" className="font-normal cursor-pointer text-slate-700 dark:text-slate-300">
Bestehende Module überschreiben
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="append" id="mode-append" />
<Label htmlFor="mode-append" className="font-normal cursor-pointer text-slate-700 dark:text-slate-300">
Module anfügen (bestehende behalten)
</Label>
</div>
</RadioGroup>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setCopySourceProduct(null)} disabled={isCopying} className="border-slate-200 dark:border-white/10 text-slate-900 dark:text-white">
Abbrechen
</Button>
<Button onClick={handleCopyModules} disabled={!copyTargetProductId || isCopying} className="bg-primary text-white hover:bg-primary/90">
{isCopying ? 'Wird kopiert...' : 'Module übertragen'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
) )
} }

View File

@@ -148,3 +148,61 @@ export async function deleteProduct(id: string) {
revalidatePath('/admin/products') revalidatePath('/admin/products')
revalidatePath('/order') 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<string, string> = {}
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')
}