feat: Add action and dialog to copy/transfer modules between products
All checks were successful
Staging Build / build (push) Successful in 2m39s
All checks were successful
Staging Build / build (push) Successful in 2m39s
This commit is contained in:
@@ -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>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user