Finished adding new commissionType
This commit is contained in:
@ -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,
|
||||||
})) || [],
|
})) || [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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)
|
||||||
|
}
|
@ -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" }],
|
||||||
});
|
});
|
||||||
|
49
src/components/drag/SortableItem.tsx
Normal file
49
src/components/drag/SortableItem.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
40
src/components/drag/SortableItemCard.tsx
Normal file
40
src/components/drag/SortableItemCard.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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 >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user