Add CommissionTypeForm

This commit is contained in:
2025-07-05 23:31:32 +02:00
parent babc1d95ba
commit 648ecda8d4
21 changed files with 1485 additions and 76 deletions

View File

@ -0,0 +1,64 @@
"use client"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma";
type CommissionTypeWithItems = CommissionType & {
options: (CommissionTypeOption & {
option: CommissionOption | null
})[]
extras: (CommissionTypeExtra & {
extra: CommissionExtra | null
})[]
}
export default function ListTypes({ types }: { types: CommissionTypeWithItems[] }) {
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>
))}
</div>
);
}

View File

@ -0,0 +1,89 @@
"use client"
import { createCommissionType } from "@/actions/items/commissions/types/newType";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { CommissionExtra, CommissionOption } from "@/generated/prisma";
import { commissionTypeSchema } from "@/schemas/commissionType";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod/v4";
import { CommissionExtraField } from "./form/CommissionExtraField";
import { CommissionOptionField } from "./form/CommissionOptionField";
type Props = {
options: CommissionOption[],
extras: CommissionExtra[],
}
export default function NewTypeForm({ options, extras }: Props) {
const router = useRouter();
const form = useForm<z.infer<typeof commissionTypeSchema>>({
resolver: zodResolver(commissionTypeSchema),
defaultValues: {
name: "",
description: "",
options: [],
extras: [],
},
})
async function onSubmit(values: z.infer<typeof commissionTypeSchema>) {
try {
const created = await createCommissionType(values)
console.log("CommissionType created:", created)
router.push("/items/commissions/types")
} catch (err) {
console.error(err)
toast("Failed to create commission type.")
}
}
return (
<div className="flex flex-col gap-8">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>The name of the commission type.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>Optional description.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<CommissionOptionField options={options} />
<CommissionExtraField extras={extras} />
<div className="flex flex-col gap-4">
<Button type="submit">Submit</Button>
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
</div>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,111 @@
"use client"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { cn } from "@/lib/utils"
import { Check, ChevronsUpDown, PlusCircle } from "lucide-react"
import { useState } from "react"
type Option = {
label: string
value: string
}
type Props = {
options: Option[]
selected: string | undefined
onSelect: (value: string) => void
onCreateNew: (name: string) => void | Promise<void>
placeholder?: string
disabled?: boolean
}
export function ComboboxCreateable({
options,
selected,
onSelect,
onCreateNew,
placeholder = "Select or create…",
disabled = false,
}: Props) {
const [open, setOpen] = useState(false)
const [input, setInput] = useState("")
const selectedOption = options.find((o) => o.value === selected)
const filteredOptions = input
? options.filter((opt) =>
opt.label.toLowerCase().includes(input.toLowerCase())
)
: options
const showCreate = input && !options.some((o) => o.label.toLowerCase() === input.toLowerCase())
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="w-full justify-between"
disabled={disabled}
>
{selectedOption?.label || placeholder}
<ChevronsUpDown className="ml-2 h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandInput
placeholder={placeholder}
value={input}
onValueChange={setInput}
/>
<CommandEmpty>No results</CommandEmpty>
<CommandGroup>
{filteredOptions.map((opt) => (
<CommandItem
key={opt.value}
onSelect={() => {
onSelect(opt.value)
setOpen(false)
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selected === opt.value ? "opacity-100" : "opacity-0"
)}
/>
{opt.label}
</CommandItem>
))}
{showCreate && (
<CommandItem
onSelect={async () => {
await onCreateNew(input)
setOpen(false)
}}
className="text-primary"
>
<PlusCircle className="mr-2 h-4 w-4" />
Create {input}
</CommandItem>
)}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
)
}

View File

@ -0,0 +1,159 @@
"use client"
import { createCommissionExtra } from "@/actions/items/commissions/types/newType"
import { Button } from "@/components/ui/button"
import { DualRangeSlider } from "@/components/ui/dual-range"
import {
FormControl,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { CommissionExtra } from "@/generated/prisma"
import { useState } from "react"
import { useFieldArray, useFormContext } from "react-hook-form"
import { ComboboxCreateable } from "./ComboboxCreateable"
type Props = {
extras: CommissionExtra[]
}
export function CommissionExtraField({ extras: initialExtras }: Props) {
const { control } = useFormContext()
const { fields, append, remove } = useFieldArray({
control,
name: "extras",
})
const [extras, setExtras] = useState(initialExtras)
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>
)}
/>
{/* 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>
)}
/>
{/* 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>
)
}}
/>
{/* Remove Button */}
<Button
type="button"
variant="destructive"
onClick={() => remove(index)}
>
Remove
</Button>
</div>
))}
<Button
type="button"
onClick={() =>
append({
extraId: "",
price: undefined,
pricePercent: undefined,
priceRange: "00",
})
}
>
Add Extra
</Button>
</div>
)
}

View File

@ -0,0 +1,160 @@
"use client"
import { createCommissionOption } from "@/actions/items/commissions/types/newType"
import { Button } from "@/components/ui/button"
import { DualRangeSlider } from "@/components/ui/dual-range"
import {
FormControl,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { CommissionOption } from "@/generated/prisma"
import { useState } from "react"
import { useFieldArray, useFormContext } from "react-hook-form"
import { ComboboxCreateable } from "./ComboboxCreateable"
type Props = {
options: CommissionOption[]
}
export function CommissionOptionField({ options: initialOptions }: Props) {
const { control } = useFormContext()
const { fields, append, remove } = useFieldArray({
control,
name: "options",
})
const [options, setOptions] = useState(initialOptions)
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>
)}
/>
{/* 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 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 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]
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>
))}
{/* Add Button */}
<Button
type="button"
onClick={() =>
append({
optionId: "",
price: undefined,
pricePercent: undefined,
priceRange: "00",
})
}
>
Add Option
</Button>
</div>
)
}