Finished adding new commissionType
This commit is contained in:
@ -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,
|
||||
})) || [],
|
||||
},
|
||||
},
|
||||
|
@ -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() {
|
||||
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" }],
|
||||
});
|
||||
|
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"
|
||||
|
||||
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 (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{types.map(type => (
|
||||
<Card key={type.id}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl truncate">{type.name}</CardTitle>
|
||||
<CardDescription>{type.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col justify-start gap-4">
|
||||
<div>
|
||||
<h4 className="font-semibold">Options</h4>
|
||||
<ul className="pl-4 list-disc">
|
||||
{type.options.map((opt) => (
|
||||
<li key={opt.id}>
|
||||
{opt.option?.name}:{" "}
|
||||
{opt.price !== null
|
||||
? `${opt.price}€`
|
||||
: opt.pricePercent
|
||||
? `+${opt.pricePercent}%`
|
||||
: opt.priceRange
|
||||
? `${opt.priceRange}€`
|
||||
: "Included"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">Extras</h4>
|
||||
<ul className="pl-4 list-disc">
|
||||
{type.extras.map((ext) => (
|
||||
<li key={ext.id}>
|
||||
{ext.extra?.name}:{" "}
|
||||
{ext.price !== null
|
||||
? `${ext.price}€`
|
||||
: ext.pricePercent
|
||||
? `+${ext.pricePercent}%`
|
||||
: ext.priceRange
|
||||
? `${ext.priceRange}€`
|
||||
: "Included"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={items.map((i) => i.id)} strategy={rectSortingStrategy}>
|
||||
{items.map(type => (
|
||||
<SortableItemCard key={type.id} id={type.id}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl truncate">{type.name}</CardTitle>
|
||||
<CardDescription>{type.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col justify-start gap-4">
|
||||
<div>
|
||||
<h4 className="font-semibold">Options</h4>
|
||||
<ul className="pl-4 list-disc">
|
||||
{type.options.map((opt) => (
|
||||
<li key={opt.id}>
|
||||
{opt.option?.name}:{" "}
|
||||
{opt.price !== null
|
||||
? `${opt.price}€`
|
||||
: opt.pricePercent
|
||||
? `+${opt.pricePercent}%`
|
||||
: opt.priceRange
|
||||
? `${opt.priceRange}€`
|
||||
: "Included"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">Extras</h4>
|
||||
<ul className="pl-4 list-disc">
|
||||
{type.extras.map((ext) => (
|
||||
<li key={ext.id}>
|
||||
{ext.extra?.name}:{" "}
|
||||
{ext.price !== null
|
||||
? `${ext.price}€`
|
||||
: ext.pricePercent
|
||||
? `+${ext.pricePercent}%`
|
||||
: ext.priceRange
|
||||
? `${ext.priceRange}€`
|
||||
: "Included"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</SortableItemCard >
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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)
|
||||
|
@ -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 (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Extras</h3>
|
||||
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="grid grid-cols-5 gap-4 items-end">
|
||||
{/* Extra Picker (combobox with create) */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`extras.${index}.extraId`}
|
||||
render={({ field: extraField }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Extra</FormLabel>
|
||||
<FormControl>
|
||||
<ComboboxCreateable
|
||||
options={extras.map((e) => ({
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={fields.map((f) => f.id)} strategy={verticalListSortingStrategy}>
|
||||
{fields.map((field, index) => (
|
||||
<SortableItem key={field.id} id={field.id}>
|
||||
<div className="grid grid-cols-5 gap-4 items-end">
|
||||
{/* Extra Picker (combobox with create) */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`extras.${index}.extraId`}
|
||||
render={({ field: extraField }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Extra</FormLabel>
|
||||
<FormControl>
|
||||
<ComboboxCreateable
|
||||
options={extras.map((e) => ({
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Price */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`extras.${index}.price`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Price (€)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Price */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`extras.${index}.price`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Price (€)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Price Percent */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`extras.${index}.pricePercent`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>+ %</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Price Percent */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`extras.${index}.pricePercent`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>+ %</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Range Slider */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`extras.${index}.priceRange`}
|
||||
render={({ field }) => {
|
||||
const [start, end] =
|
||||
typeof field.value === "string" && field.value.includes("–")
|
||||
? field.value.split("–").map(Number)
|
||||
: [0, 0]
|
||||
{/* Range Slider */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`extras.${index}.priceRange`}
|
||||
render={({ field }) => {
|
||||
const [start, end] =
|
||||
typeof field.value === "string" && field.value.includes("–")
|
||||
? field.value.split("–").map(Number)
|
||||
: [0, 0]
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Range</FormLabel>
|
||||
<FormControl>
|
||||
<DualRangeSlider
|
||||
value={[start, end]}
|
||||
onChange={([min, max]) =>
|
||||
field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Range</FormLabel>
|
||||
<FormControl>
|
||||
<DualRangeSlider
|
||||
value={[start, end]}
|
||||
onChange={([min, max]) =>
|
||||
field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Remove Button */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{/* Remove Button */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</SortableItem>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext >
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
@ -154,6 +190,6 @@ export function CommissionExtraField({ extras: initialExtras }: Props) {
|
||||
>
|
||||
Add Extra
|
||||
</Button>
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
@ -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 (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Options</h3>
|
||||
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="grid grid-cols-5 gap-4 items-end">
|
||||
{/* Option Picker (combobox with create) */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`options.${index}.optionId`}
|
||||
render={({ field: optionField }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Option</FormLabel>
|
||||
<FormControl>
|
||||
<ComboboxCreateable
|
||||
options={options.map((o) => ({
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={fields.map((f) => f.id)} strategy={verticalListSortingStrategy}>
|
||||
|
||||
{/* Price */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`options.${index}.price`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Price (€)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{fields.map((field, index) => (
|
||||
<SortableItem key={field.id} id={field.id}>
|
||||
<div className="grid grid-cols-5 gap-4 items-end">
|
||||
{/* Option Picker (combobox with create) */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`options.${index}.optionId`}
|
||||
render={({ field: optionField }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Option</FormLabel>
|
||||
<FormControl>
|
||||
<ComboboxCreateable
|
||||
options={options.map((o) => ({
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Price Percent */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`options.${index}.pricePercent`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>+ %</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Price */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`options.${index}.price`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Price (€)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Price Range Slider */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`options.${index}.priceRange`}
|
||||
render={({ field }) => {
|
||||
const [start, end] =
|
||||
typeof field.value === "string" && field.value.includes("–")
|
||||
? field.value.split("–").map(Number)
|
||||
: [0, 0]
|
||||
{/* Price Percent */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`options.${index}.pricePercent`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>+ %</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.value === "" ? undefined : e.target.valueAsNumber)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Range</FormLabel>
|
||||
<FormControl>
|
||||
<DualRangeSlider
|
||||
value={[start, end]}
|
||||
onChange={([min, max]) =>
|
||||
field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/* Price Range Slider */}
|
||||
<FormField
|
||||
control={control}
|
||||
name={`options.${index}.priceRange`}
|
||||
render={({ field }) => {
|
||||
const [start, end] =
|
||||
typeof field.value === "string" && field.value.includes("–")
|
||||
? field.value.split("–").map(Number)
|
||||
: [0, 0]
|
||||
|
||||
{/* Remove Button */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Range</FormLabel>
|
||||
<FormControl>
|
||||
<DualRangeSlider
|
||||
value={[start, end]}
|
||||
onChange={([min, max]) =>
|
||||
field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Remove Button */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</SortableItem>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{/* Add Button */}
|
||||
<Button
|
||||
|
Reference in New Issue
Block a user