diff --git a/src/actions/items/commissions/types/newType.ts b/src/actions/items/commissions/types/newType.ts
index 5a399a7..b9db59b 100644
--- a/src/actions/items/commissions/types/newType.ts
+++ b/src/actions/items/commissions/types/newType.ts
@@ -36,19 +36,21 @@ export async function createCommissionType(formData: commissionTypeSchema) {
name: data.name,
description: data.description,
options: {
- create: data.options?.map((opt) => ({
+ create: data.options?.map((opt, index) => ({
option: { connect: { id: opt.optionId } },
price: opt.price,
pricePercent: opt.pricePercent,
priceRange: opt.priceRange,
+ sortIndex: index,
})) || [],
},
extras: {
- create: data.extras?.map((ext) => ({
+ create: data.extras?.map((ext, index) => ({
extra: { connect: { id: ext.extraId } },
price: ext.price,
pricePercent: ext.pricePercent,
priceRange: ext.priceRange,
+ sortIndex: index,
})) || [],
},
},
diff --git a/src/actions/items/commissions/types/updateCommissionTypeSortOrder.ts b/src/actions/items/commissions/types/updateCommissionTypeSortOrder.ts
new file mode 100644
index 0000000..6ac70ad
--- /dev/null
+++ b/src/actions/items/commissions/types/updateCommissionTypeSortOrder.ts
@@ -0,0 +1,16 @@
+"use server"
+
+import prisma from "@/lib/prisma";
+
+export async function updateCommissionTypeSortOrder(
+ ordered: { id: string; sortIndex: number }[]
+) {
+ const updates = ordered.map(({ id, sortIndex }) =>
+ prisma.commissionType.update({
+ where: { id },
+ data: { sortIndex },
+ })
+ )
+
+ await Promise.all(updates)
+}
\ No newline at end of file
diff --git a/src/app/items/commissions/types/page.tsx b/src/app/items/commissions/types/page.tsx
index e518f69..0314564 100644
--- a/src/app/items/commissions/types/page.tsx
+++ b/src/app/items/commissions/types/page.tsx
@@ -6,8 +6,8 @@ import Link from "next/link";
export default async function CommissionTypesPage() {
const types = await prisma.commissionType.findMany({
include: {
- options: { include: { option: true } },
- extras: { include: { extra: true } },
+ options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
+ extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
},
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
});
diff --git a/src/components/drag/SortableItem.tsx b/src/components/drag/SortableItem.tsx
new file mode 100644
index 0000000..d5a84fe
--- /dev/null
+++ b/src/components/drag/SortableItem.tsx
@@ -0,0 +1,49 @@
+import {
+ useSortable
+} from "@dnd-kit/sortable"
+import { CSS } from "@dnd-kit/utilities"
+
+export default function SortableItem({
+ id,
+ children,
+}: {
+ id: string
+ children: React.ReactNode
+}) {
+ const {
+ attributes,
+ listeners, // 👈 the key to handle-only dragging
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id })
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ zIndex: isDragging ? 50 : "auto",
+ opacity: isDragging ? 0.7 : 1,
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/drag/SortableItemCard.tsx b/src/components/drag/SortableItemCard.tsx
new file mode 100644
index 0000000..b065b8f
--- /dev/null
+++ b/src/components/drag/SortableItemCard.tsx
@@ -0,0 +1,40 @@
+"use client"
+
+import { useSortable } from "@dnd-kit/sortable"
+import { CSS } from "@dnd-kit/utilities"
+import { ReactNode } from "react"
+
+type Props = {
+ id: string
+ children: ReactNode
+}
+
+export default function SortableItemCard({ id, children }: Props) {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id })
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ zIndex: isDragging ? 40 : "auto",
+ opacity: isDragging ? 0.7 : 1,
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/items/commissions/types/ListTypes.tsx b/src/components/items/commissions/types/ListTypes.tsx
index 9066c80..589ee76 100644
--- a/src/components/items/commissions/types/ListTypes.tsx
+++ b/src/components/items/commissions/types/ListTypes.tsx
@@ -1,7 +1,23 @@
"use client"
+import { updateCommissionTypeSortOrder } from "@/actions/items/commissions/types/updateCommissionTypeSortOrder";
+import SortableItemCard from "@/components/drag/SortableItemCard";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma";
+import {
+ closestCenter,
+ DndContext,
+ DragEndEvent,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from "@dnd-kit/core";
+import {
+ arrayMove,
+ rectSortingStrategy,
+ SortableContext
+} from "@dnd-kit/sortable";
+import { useEffect, useState } from "react";
type CommissionTypeWithItems = CommissionType & {
options: (CommissionTypeOption & {
@@ -13,52 +29,85 @@ type CommissionTypeWithItems = CommissionType & {
}
export default function ListTypes({ types }: { types: CommissionTypeWithItems[] }) {
+ const [items, setItems] = useState(types)
+ const [isMounted, setIsMounted] = useState(false)
+
+ useEffect(() => {
+ setIsMounted(true)
+ }, [])
+
+ const sensors = useSensors(useSensor(PointerSensor))
+
+ const handleDragEnd = async (event: DragEndEvent) => {
+ const { active, over } = event
+
+ if (!over || active.id === over.id) return
+
+ const oldIndex = items.findIndex((i) => i.id === active.id)
+ const newIndex = items.findIndex((i) => i.id === over.id)
+
+ if (oldIndex !== -1 && newIndex !== -1) {
+ const newItems = arrayMove(items, oldIndex, newIndex)
+ setItems(newItems)
+
+ await updateCommissionTypeSortOrder(newItems.map((item, i) => ({ id: item.id, sortIndex: i })))
+ }
+ }
+
+ if (!isMounted) return null
+
return (
- {types.map(type => (
-
-
- {type.name}
- {type.description}
-
-
-
-
Options
-
- {type.options.map((opt) => (
- -
- {opt.option?.name}:{" "}
- {opt.price !== null
- ? `${opt.price}€`
- : opt.pricePercent
- ? `+${opt.pricePercent}%`
- : opt.priceRange
- ? `${opt.priceRange}€`
- : "Included"}
-
- ))}
-
-
-
-
Extras
-
- {type.extras.map((ext) => (
- -
- {ext.extra?.name}:{" "}
- {ext.price !== null
- ? `${ext.price}€`
- : ext.pricePercent
- ? `+${ext.pricePercent}%`
- : ext.priceRange
- ? `${ext.priceRange}€`
- : "Included"}
-
- ))}
-
-
-
-
- ))}
+
+ i.id)} strategy={rectSortingStrategy}>
+ {items.map(type => (
+
+
+
+ {type.name}
+ {type.description}
+
+
+
+
Options
+
+ {type.options.map((opt) => (
+ -
+ {opt.option?.name}:{" "}
+ {opt.price !== null
+ ? `${opt.price}€`
+ : opt.pricePercent
+ ? `+${opt.pricePercent}%`
+ : opt.priceRange
+ ? `${opt.priceRange}€`
+ : "Included"}
+
+ ))}
+
+
+
+
Extras
+
+ {type.extras.map((ext) => (
+ -
+ {ext.extra?.name}:{" "}
+ {ext.price !== null
+ ? `${ext.price}€`
+ : ext.pricePercent
+ ? `+${ext.pricePercent}%`
+ : ext.priceRange
+ ? `${ext.priceRange}€`
+ : "Included"}
+
+ ))}
+
+
+
+
+
+ ))}
+
+
);
}
\ No newline at end of file
diff --git a/src/components/items/commissions/types/NewTypeForm.tsx b/src/components/items/commissions/types/NewTypeForm.tsx
index 14d202d..a6e5a2e 100644
--- a/src/components/items/commissions/types/NewTypeForm.tsx
+++ b/src/components/items/commissions/types/NewTypeForm.tsx
@@ -35,6 +35,7 @@ export default function NewTypeForm({ options, extras }: Props) {
try {
const created = await createCommissionType(values)
console.log("CommissionType created:", created)
+ toast("Commission type created.")
router.push("/items/commissions/types")
} catch (err) {
console.error(err)
diff --git a/src/components/items/commissions/types/form/CommissionExtraField.tsx b/src/components/items/commissions/types/form/CommissionExtraField.tsx
index 4a6559e..fa14594 100644
--- a/src/components/items/commissions/types/form/CommissionExtraField.tsx
+++ b/src/components/items/commissions/types/form/CommissionExtraField.tsx
@@ -1,6 +1,7 @@
"use client"
import { createCommissionExtra } from "@/actions/items/commissions/types/newType"
+import SortableItem from "@/components/drag/SortableItem"
import { Button } from "@/components/ui/button"
import { DualRangeSlider } from "@/components/ui/dual-range"
import {
@@ -11,6 +12,18 @@ import {
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { CommissionExtra } from "@/generated/prisma"
+import {
+ closestCenter,
+ DndContext,
+ DragEndEvent,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from "@dnd-kit/core"
+import {
+ SortableContext,
+ verticalListSortingStrategy
+} from "@dnd-kit/sortable"
import { useState } from "react"
import { useFieldArray, useFormContext } from "react-hook-form"
import { ComboboxCreateable } from "./ComboboxCreateable"
@@ -21,125 +34,148 @@ type Props = {
export function CommissionExtraField({ extras: initialExtras }: Props) {
const { control } = useFormContext()
- const { fields, append, remove } = useFieldArray({
+ const { fields, append, remove, move } = useFieldArray({
control,
name: "extras",
})
const [extras, setExtras] = useState(initialExtras)
+ const sensors = useSensors(useSensor(PointerSensor))
+
+ const handleDragEnd = (event: DragEndEvent) => {
+ const { active, over } = event
+
+ if (!over || active.id === over.id) {
+ return
+ }
+
+ const oldIndex = fields.findIndex((f) => f.id === active.id)
+ const newIndex = fields.findIndex((f) => f.id === over.id)
+
+ if (oldIndex !== -1 && newIndex !== -1) {
+ move(oldIndex, newIndex)
+ }
+ }
+
return (
Extras
- {fields.map((field, index) => (
-
- {/* Extra Picker (combobox with create) */}
-
(
-
- Extra
-
- ({
- label: e.name,
- value: e.id,
- }))}
- selected={extraField.value}
- onSelect={extraField.onChange}
- onCreateNew={async (name) => {
- const newExtra = await createCommissionExtra({ name })
- setExtras((prev) => [...prev, newExtra])
- extraField.onChange(newExtra.id)
- }}
- />
-
-
- )}
- />
+
+ f.id)} strategy={verticalListSortingStrategy}>
+ {fields.map((field, index) => (
+
+
+ {/* Extra Picker (combobox with create) */}
+ (
+
+ Extra
+
+ ({
+ label: e.name,
+ value: e.id,
+ }))}
+ selected={extraField.value}
+ onSelect={extraField.onChange}
+ onCreateNew={async (name) => {
+ const newExtra = await createCommissionExtra({ name })
+ setExtras((prev) => [...prev, newExtra])
+ extraField.onChange(newExtra.id)
+ }}
+ />
+
+
+ )}
+ />
- {/* Price */}
- (
-
- Price (€)
-
-
- field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
- }
- />
-
-
- )}
- />
+ {/* Price */}
+ (
+
+ Price (€)
+
+
+ field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
+ }
+ />
+
+
+ )}
+ />
- {/* Price Percent */}
- (
-
- + %
-
-
- field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
- }
- />
-
-
- )}
- />
+ {/* Price Percent */}
+ (
+
+ + %
+
+
+ field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
+ }
+ />
+
+
+ )}
+ />
- {/* Range Slider */}
- {
- const [start, end] =
- typeof field.value === "string" && field.value.includes("–")
- ? field.value.split("–").map(Number)
- : [0, 0]
+ {/* Range Slider */}
+ {
+ const [start, end] =
+ typeof field.value === "string" && field.value.includes("–")
+ ? field.value.split("–").map(Number)
+ : [0, 0]
- return (
-
- Range
-
-
- field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
- }
- />
-
-
- )
- }}
- />
+ return (
+
+ Range
+
+
+ field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
+ }
+ />
+
+
+ )
+ }}
+ />
- {/* Remove Button */}
-
-
- ))}
+ {/* Remove Button */}
+
+
+
+ ))}
+
+
-
+
)
}
diff --git a/src/components/items/commissions/types/form/CommissionOptionField.tsx b/src/components/items/commissions/types/form/CommissionOptionField.tsx
index 50f9746..4778fad 100644
--- a/src/components/items/commissions/types/form/CommissionOptionField.tsx
+++ b/src/components/items/commissions/types/form/CommissionOptionField.tsx
@@ -1,6 +1,7 @@
"use client"
import { createCommissionOption } from "@/actions/items/commissions/types/newType"
+import SortableItem from "@/components/drag/SortableItem"
import { Button } from "@/components/ui/button"
import { DualRangeSlider } from "@/components/ui/dual-range"
import {
@@ -11,6 +12,18 @@ import {
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { CommissionOption } from "@/generated/prisma"
+import {
+ closestCenter,
+ DndContext,
+ DragEndEvent,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from "@dnd-kit/core"
+import {
+ SortableContext,
+ verticalListSortingStrategy
+} from "@dnd-kit/sortable"
import { useState } from "react"
import { useFieldArray, useFormContext } from "react-hook-form"
import { ComboboxCreateable } from "./ComboboxCreateable"
@@ -21,125 +34,149 @@ type Props = {
export function CommissionOptionField({ options: initialOptions }: Props) {
const { control } = useFormContext()
- const { fields, append, remove } = useFieldArray({
+ const { fields, append, remove, move } = useFieldArray({
control,
name: "options",
})
const [options, setOptions] = useState(initialOptions)
+ const sensors = useSensors(useSensor(PointerSensor))
+
+ const handleDragEnd = (event: DragEndEvent) => {
+ const { active, over } = event
+
+ if (!over || active.id === over.id) {
+ return
+ }
+
+ const oldIndex = fields.findIndex((f) => f.id === active.id)
+ const newIndex = fields.findIndex((f) => f.id === over.id)
+
+ if (oldIndex !== -1 && newIndex !== -1) {
+ move(oldIndex, newIndex)
+ }
+ }
+
return (
Options
- {fields.map((field, index) => (
-
- {/* Option Picker (combobox with create) */}
-
(
-
- Option
-
- ({
- label: o.name,
- value: o.id,
- }))}
- selected={optionField.value}
- onSelect={optionField.onChange}
- onCreateNew={async (name) => {
- const newOption = await createCommissionOption({ name })
- setOptions((prev) => [...prev, newOption])
- optionField.onChange(newOption.id)
- }}
- />
-
-
- )}
- />
+
+ f.id)} strategy={verticalListSortingStrategy}>
- {/* Price */}
- (
-
- Price (€)
-
-
- field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
- }
- />
-
-
- )}
- />
+ {fields.map((field, index) => (
+
+
+ {/* Option Picker (combobox with create) */}
+ (
+
+ Option
+
+ ({
+ label: o.name,
+ value: o.id,
+ }))}
+ selected={optionField.value}
+ onSelect={optionField.onChange}
+ onCreateNew={async (name) => {
+ const newOption = await createCommissionOption({ name })
+ setOptions((prev) => [...prev, newOption])
+ optionField.onChange(newOption.id)
+ }}
+ />
+
+
+ )}
+ />
- {/* Price Percent */}
- (
-
- + %
-
-
- field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
- }
- />
-
-
- )}
- />
+ {/* Price */}
+ (
+
+ Price (€)
+
+
+ field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
+ }
+ />
+
+
+ )}
+ />
- {/* Price Range Slider */}
- {
- const [start, end] =
- typeof field.value === "string" && field.value.includes("–")
- ? field.value.split("–").map(Number)
- : [0, 0]
+ {/* Price Percent */}
+ (
+
+ + %
+
+
+ field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
+ }
+ />
+
+
+ )}
+ />
- return (
-
- Range
-
-
- field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
- }
- />
-
-
- )
- }}
- />
+ {/* Price Range Slider */}
+ {
+ const [start, end] =
+ typeof field.value === "string" && field.value.includes("–")
+ ? field.value.split("–").map(Number)
+ : [0, 0]
- {/* Remove Button */}
-
-
- ))}
+ return (
+
+ Range
+
+
+ field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
+ }
+ />
+
+
+ )
+ }}
+ />
+
+ {/* Remove Button */}
+
+
+
+ ))}
+
+
{/* Add Button */}