Finished adding new commissionType

This commit is contained in:
2025-07-06 00:31:41 +02:00
parent 648ecda8d4
commit a0b515366a
9 changed files with 487 additions and 257 deletions

View File

@ -36,19 +36,21 @@ export async function createCommissionType(formData: commissionTypeSchema) {
name: data.name, name: data.name,
description: data.description, description: data.description,
options: { options: {
create: data.options?.map((opt) => ({ create: data.options?.map((opt, index) => ({
option: { connect: { id: opt.optionId } }, option: { connect: { id: opt.optionId } },
price: opt.price, price: opt.price,
pricePercent: opt.pricePercent, pricePercent: opt.pricePercent,
priceRange: opt.priceRange, priceRange: opt.priceRange,
sortIndex: index,
})) || [], })) || [],
}, },
extras: { extras: {
create: data.extras?.map((ext) => ({ create: data.extras?.map((ext, index) => ({
extra: { connect: { id: ext.extraId } }, extra: { connect: { id: ext.extraId } },
price: ext.price, price: ext.price,
pricePercent: ext.pricePercent, pricePercent: ext.pricePercent,
priceRange: ext.priceRange, priceRange: ext.priceRange,
sortIndex: index,
})) || [], })) || [],
}, },
}, },

View File

@ -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)
}

View File

@ -6,8 +6,8 @@ import Link from "next/link";
export default async function CommissionTypesPage() { export default async function CommissionTypesPage() {
const types = await prisma.commissionType.findMany({ const types = await prisma.commissionType.findMany({
include: { include: {
options: { include: { option: true } }, options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
extras: { include: { extra: true } }, extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
}, },
orderBy: [{ sortIndex: "asc" }, { name: "asc" }], orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
}); });

View File

@ -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 (
<div
ref={setNodeRef}
style={style}
className={`transition-all duration-200 ease-in-out ${isDragging ? "ring-2 ring-primary rounded-md shadow-lg" : ""
}`}
>
<div className="flex items-start gap-2 min-h-[4rem]">
<div
{...listeners}
{...attributes}
className="cursor-grab px-1 pt-2 text-muted-foreground select-none"
title="Drag to reorder"
>
</div>
<div className="w-full">{children}</div>
</div>
</div>
)
}

View File

@ -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 (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="cursor-grab"
>
{children}
</div>
)
}

View File

@ -1,7 +1,23 @@
"use client" "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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma"; 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 & { type CommissionTypeWithItems = CommissionType & {
options: (CommissionTypeOption & { options: (CommissionTypeOption & {
@ -13,10 +29,40 @@ type CommissionTypeWithItems = CommissionType & {
} }
export default function ListTypes({ types }: { types: CommissionTypeWithItems[] }) { 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 ( return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{types.map(type => ( <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<Card key={type.id}> <SortableContext items={items.map((i) => i.id)} strategy={rectSortingStrategy}>
{items.map(type => (
<SortableItemCard key={type.id} id={type.id}>
<Card>
<CardHeader> <CardHeader>
<CardTitle className="text-xl truncate">{type.name}</CardTitle> <CardTitle className="text-xl truncate">{type.name}</CardTitle>
<CardDescription>{type.description}</CardDescription> <CardDescription>{type.description}</CardDescription>
@ -58,7 +104,10 @@ export default function ListTypes({ types }: { types: CommissionTypeWithItems[]
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</SortableItemCard >
))} ))}
</SortableContext>
</DndContext>
</div> </div>
); );
} }

View File

@ -35,6 +35,7 @@ export default function NewTypeForm({ options, extras }: Props) {
try { try {
const created = await createCommissionType(values) const created = await createCommissionType(values)
console.log("CommissionType created:", created) console.log("CommissionType created:", created)
toast("Commission type created.")
router.push("/items/commissions/types") router.push("/items/commissions/types")
} catch (err) { } catch (err) {
console.error(err) console.error(err)

View File

@ -1,6 +1,7 @@
"use client" "use client"
import { createCommissionExtra } from "@/actions/items/commissions/types/newType" import { createCommissionExtra } from "@/actions/items/commissions/types/newType"
import SortableItem from "@/components/drag/SortableItem"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { DualRangeSlider } from "@/components/ui/dual-range" import { DualRangeSlider } from "@/components/ui/dual-range"
import { import {
@ -11,6 +12,18 @@ import {
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { CommissionExtra } from "@/generated/prisma" 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 { useState } from "react"
import { useFieldArray, useFormContext } from "react-hook-form" import { useFieldArray, useFormContext } from "react-hook-form"
import { ComboboxCreateable } from "./ComboboxCreateable" import { ComboboxCreateable } from "./ComboboxCreateable"
@ -21,19 +34,39 @@ type Props = {
export function CommissionExtraField({ extras: initialExtras }: Props) { export function CommissionExtraField({ extras: initialExtras }: Props) {
const { control } = useFormContext() const { control } = useFormContext()
const { fields, append, remove } = useFieldArray({ const { fields, append, remove, move } = useFieldArray({
control, control,
name: "extras", name: "extras",
}) })
const [extras, setExtras] = useState(initialExtras) 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 ( return (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-semibold">Extras</h3> <h3 className="text-lg font-semibold">Extras</h3>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={fields.map((f) => f.id)} strategy={verticalListSortingStrategy}>
{fields.map((field, index) => ( {fields.map((field, index) => (
<div key={field.id} className="grid grid-cols-5 gap-4 items-end"> <SortableItem key={field.id} id={field.id}>
<div className="grid grid-cols-5 gap-4 items-end">
{/* Extra Picker (combobox with create) */} {/* Extra Picker (combobox with create) */}
<FormField <FormField
control={control} control={control}
@ -139,7 +172,10 @@ export function CommissionExtraField({ extras: initialExtras }: Props) {
Remove Remove
</Button> </Button>
</div> </div>
</SortableItem>
))} ))}
</SortableContext>
</DndContext >
<Button <Button
type="button" type="button"
@ -154,6 +190,6 @@ export function CommissionExtraField({ extras: initialExtras }: Props) {
> >
Add Extra Add Extra
</Button> </Button>
</div> </div >
) )
} }

View File

@ -1,6 +1,7 @@
"use client" "use client"
import { createCommissionOption } from "@/actions/items/commissions/types/newType" import { createCommissionOption } from "@/actions/items/commissions/types/newType"
import SortableItem from "@/components/drag/SortableItem"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { DualRangeSlider } from "@/components/ui/dual-range" import { DualRangeSlider } from "@/components/ui/dual-range"
import { import {
@ -11,6 +12,18 @@ import {
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { CommissionOption } from "@/generated/prisma" 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 { useState } from "react"
import { useFieldArray, useFormContext } from "react-hook-form" import { useFieldArray, useFormContext } from "react-hook-form"
import { ComboboxCreateable } from "./ComboboxCreateable" import { ComboboxCreateable } from "./ComboboxCreateable"
@ -21,19 +34,40 @@ type Props = {
export function CommissionOptionField({ options: initialOptions }: Props) { export function CommissionOptionField({ options: initialOptions }: Props) {
const { control } = useFormContext() const { control } = useFormContext()
const { fields, append, remove } = useFieldArray({ const { fields, append, remove, move } = useFieldArray({
control, control,
name: "options", name: "options",
}) })
const [options, setOptions] = useState(initialOptions) 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 ( return (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-semibold">Options</h3> <h3 className="text-lg font-semibold">Options</h3>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={fields.map((f) => f.id)} strategy={verticalListSortingStrategy}>
{fields.map((field, index) => ( {fields.map((field, index) => (
<div key={field.id} className="grid grid-cols-5 gap-4 items-end"> <SortableItem key={field.id} id={field.id}>
<div className="grid grid-cols-5 gap-4 items-end">
{/* Option Picker (combobox with create) */} {/* Option Picker (combobox with create) */}
<FormField <FormField
control={control} control={control}
@ -139,7 +173,10 @@ export function CommissionOptionField({ options: initialOptions }: Props) {
Remove Remove
</Button> </Button>
</div> </div>
</SortableItem>
))} ))}
</SortableContext>
</DndContext>
{/* Add Button */} {/* Add Button */}
<Button <Button