diff --git a/prisma/migrations/20250707151218_type_field/migration.sql b/prisma/migrations/20250707151218_type_field/migration.sql
new file mode 100644
index 0000000..04f6229
--- /dev/null
+++ b/prisma/migrations/20250707151218_type_field/migration.sql
@@ -0,0 +1,34 @@
+-- CreateTable
+CREATE TABLE "CommissionField" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "sortIndex" INTEGER NOT NULL DEFAULT 0,
+ "fieldType" TEXT NOT NULL,
+ "label" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "required" BOOLEAN NOT NULL DEFAULT false,
+
+ CONSTRAINT "CommissionField_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "CommissionTypeField" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "sortIndex" INTEGER NOT NULL DEFAULT 0,
+ "typeId" TEXT NOT NULL,
+ "fieldId" TEXT NOT NULL,
+
+ CONSTRAINT "CommissionTypeField_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "CommissionTypeField_typeId_fieldId_key" ON "CommissionTypeField"("typeId", "fieldId");
+
+-- AddForeignKey
+ALTER TABLE "CommissionTypeField" ADD CONSTRAINT "CommissionTypeField_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CommissionType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "CommissionTypeField" ADD CONSTRAINT "CommissionTypeField_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "CommissionField"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250707160417_type_custom_inputs/migration.sql b/prisma/migrations/20250707160417_type_custom_inputs/migration.sql
new file mode 100644
index 0000000..47cb8cb
--- /dev/null
+++ b/prisma/migrations/20250707160417_type_custom_inputs/migration.sql
@@ -0,0 +1,53 @@
+/*
+ Warnings:
+
+ - You are about to drop the `CommissionField` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `CommissionTypeField` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "CommissionTypeField" DROP CONSTRAINT "CommissionTypeField_fieldId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "CommissionTypeField" DROP CONSTRAINT "CommissionTypeField_typeId_fkey";
+
+-- DropTable
+DROP TABLE "CommissionField";
+
+-- DropTable
+DROP TABLE "CommissionTypeField";
+
+-- CreateTable
+CREATE TABLE "CommissionCustomInput" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "sortIndex" INTEGER NOT NULL DEFAULT 0,
+ "inputType" TEXT NOT NULL,
+ "label" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "required" BOOLEAN NOT NULL DEFAULT false,
+
+ CONSTRAINT "CommissionCustomInput_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "CommissionTypeCustomInput" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "sortIndex" INTEGER NOT NULL DEFAULT 0,
+ "typeId" TEXT NOT NULL,
+ "customInputId" TEXT NOT NULL,
+
+ CONSTRAINT "CommissionTypeCustomInput_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "CommissionTypeCustomInput_typeId_customInputId_key" ON "CommissionTypeCustomInput"("typeId", "customInputId");
+
+-- AddForeignKey
+ALTER TABLE "CommissionTypeCustomInput" ADD CONSTRAINT "CommissionTypeCustomInput_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CommissionType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "CommissionTypeCustomInput" ADD CONSTRAINT "CommissionTypeCustomInput_customInputId_fkey" FOREIGN KEY ("customInputId") REFERENCES "CommissionCustomInput"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250707170451_type_custom_inputs/migration.sql b/prisma/migrations/20250707170451_type_custom_inputs/migration.sql
new file mode 100644
index 0000000..60ff8f1
--- /dev/null
+++ b/prisma/migrations/20250707170451_type_custom_inputs/migration.sql
@@ -0,0 +1,25 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `inputType` on the `CommissionCustomInput` table. All the data in the column will be lost.
+ - You are about to drop the column `label` on the `CommissionCustomInput` table. All the data in the column will be lost.
+ - You are about to drop the column `required` on the `CommissionCustomInput` table. All the data in the column will be lost.
+ - A unique constraint covering the columns `[name]` on the table `CommissionCustomInput` will be added. If there are existing duplicate values, this will fail.
+ - Added the required column `fieldId` to the `CommissionCustomInput` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `inputType` to the `CommissionTypeCustomInput` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `label` to the `CommissionTypeCustomInput` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "CommissionCustomInput" DROP COLUMN "inputType",
+DROP COLUMN "label",
+DROP COLUMN "required",
+ADD COLUMN "fieldId" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "CommissionTypeCustomInput" ADD COLUMN "inputType" TEXT NOT NULL,
+ADD COLUMN "label" TEXT NOT NULL,
+ADD COLUMN "required" BOOLEAN NOT NULL DEFAULT false;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "CommissionCustomInput_name_key" ON "CommissionCustomInput"("name");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index f376ca0..13a5c6a 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -24,8 +24,9 @@ model CommissionType {
description String?
- options CommissionTypeOption[]
- extras CommissionTypeExtra[]
+ options CommissionTypeOption[]
+ extras CommissionTypeExtra[]
+ customInputs CommissionTypeCustomInput[]
}
model CommissionOption {
@@ -54,6 +55,18 @@ model CommissionExtra {
types CommissionTypeExtra[]
}
+model CommissionCustomInput {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ sortIndex Int @default(0)
+
+ name String @unique
+ fieldId String
+
+ types CommissionTypeCustomInput[]
+}
+
model CommissionTypeOption {
id String @id @default(cuid())
createdAt DateTime @default(now())
@@ -92,6 +105,25 @@ model CommissionTypeExtra {
@@unique([typeId, extraId])
}
+model CommissionTypeCustomInput {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ sortIndex Int @default(0)
+
+ typeId String
+ customInputId String
+
+ inputType String
+ label String
+ required Boolean @default(false)
+
+ type CommissionType @relation(fields: [typeId], references: [id])
+ customInput CommissionCustomInput @relation(fields: [customInputId], references: [id])
+
+ @@unique([typeId, customInputId])
+}
+
model TermsOfService {
id String @id @default(cuid())
createdAt DateTime @default(now())
diff --git a/src/actions/items/commissions/types/newType.ts b/src/actions/items/commissions/types/newType.ts
index b9db59b..2777265 100644
--- a/src/actions/items/commissions/types/newType.ts
+++ b/src/actions/items/commissions/types/newType.ts
@@ -21,6 +21,18 @@ export async function createCommissionExtra(data: { name: string }) {
})
}
+export async function createCommissionCustomInput(data: {
+ name: string
+ fieldId: string
+}) {
+ return await prisma.commissionCustomInput.create({
+ data: {
+ name: data.name,
+ fieldId: data.fieldId,
+ },
+ })
+}
+
export async function createCommissionType(formData: commissionTypeSchema) {
const parsed = commissionTypeSchema.safeParse(formData)
@@ -53,6 +65,15 @@ export async function createCommissionType(formData: commissionTypeSchema) {
sortIndex: index,
})) || [],
},
+ customInputs: {
+ create: data.customInputs?.map((c, index) => ({
+ customInput: { connect: { id: c.customInputId } },
+ label: c.label,
+ inputType: c.inputType,
+ required: c.required,
+ sortIndex: index,
+ })) || [],
+ },
},
})
diff --git a/src/actions/items/commissions/types/updateType.ts b/src/actions/items/commissions/types/updateType.ts
index 30195ea..ce73d2c 100644
--- a/src/actions/items/commissions/types/updateType.ts
+++ b/src/actions/items/commissions/types/updateType.ts
@@ -35,10 +35,21 @@ export async function updateCommissionType(
sortIndex: index,
})),
},
+ customInputs: {
+ deleteMany: {},
+ create: data.customInputs?.map((c, index) => ({
+ customInput: { connect: { id: c.customInputId } },
+ label: c.label,
+ inputType: c.inputType,
+ required: c.required,
+ sortIndex: index,
+ })) || [],
+ },
},
include: {
options: true,
extras: true,
+ customInputs: true,
},
})
diff --git a/src/app/items/commissions/types/[id]/edit/page.tsx b/src/app/items/commissions/types/[id]/edit/page.tsx
index 9e1e842..49a847c 100644
--- a/src/app/items/commissions/types/[id]/edit/page.tsx
+++ b/src/app/items/commissions/types/[id]/edit/page.tsx
@@ -10,6 +10,7 @@ export default async function CommissionTypesEditPage({ params }: { params: { id
include: {
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
+ customInputs: { include: { customInput: true }, orderBy: { sortIndex: "asc" } },
},
})
const options = await prisma.commissionOption.findMany({
@@ -18,6 +19,9 @@ export default async function CommissionTypesEditPage({ params }: { params: { id
const extras = await prisma.commissionExtra.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
})
+ const customInputs = await prisma.commissionCustomInput.findMany({
+ orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
+ })
if (!commissionType) {
return
Type not found
@@ -28,7 +32,7 @@ export default async function CommissionTypesEditPage({ params }: { params: { id
Edit Commission Type
-
+
);
}
\ No newline at end of file
diff --git a/src/app/items/commissions/types/new/page.tsx b/src/app/items/commissions/types/new/page.tsx
index 9c50035..08bf89c 100644
--- a/src/app/items/commissions/types/new/page.tsx
+++ b/src/app/items/commissions/types/new/page.tsx
@@ -8,6 +8,9 @@ export default async function CommissionTypesNewPage() {
const extras = await prisma.commissionExtra.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
})
+ const customInputs = await prisma.commissionCustomInput.findMany({
+ orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
+ })
return (
@@ -15,7 +18,7 @@ export default async function CommissionTypesNewPage() {
New Commission Type
-
+
);
}
\ 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 0314564..a5a3af5 100644
--- a/src/app/items/commissions/types/page.tsx
+++ b/src/app/items/commissions/types/page.tsx
@@ -8,6 +8,7 @@ export default async function CommissionTypesPage() {
include: {
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
+ customInputs: { include: { customInput: true }, orderBy: { sortIndex: "asc" } },
},
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
});
diff --git a/src/components/items/commissions/types/EditTypeForm.tsx b/src/components/items/commissions/types/EditTypeForm.tsx
index 9344559..2294861 100644
--- a/src/components/items/commissions/types/EditTypeForm.tsx
+++ b/src/components/items/commissions/types/EditTypeForm.tsx
@@ -4,28 +4,31 @@ import { updateCommissionType } from "@/actions/items/commissions/types/updateTy
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, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma";
+import { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionType, CommissionTypeCustomInput, CommissionTypeExtra, CommissionTypeOption } 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 { CommissionCustomInputField } from "./form/CommissionCustomInputField";
import { CommissionExtraField } from "./form/CommissionExtraField";
import { CommissionOptionField } from "./form/CommissionOptionField";
type CommissionTypeWithConnections = CommissionType & {
options: (CommissionTypeOption & { option: CommissionOption })[]
extras: (CommissionTypeExtra & { extra: CommissionExtra })[]
+ customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[]
}
type Props = {
type: CommissionTypeWithConnections
allOptions: CommissionOption[],
allExtras: CommissionExtra[],
+ allCustomInputs: CommissionCustomInput[]
}
-export default function EditTypeForm({ type, allOptions, allExtras }: Props) {
+export default function EditTypeForm({ type, allOptions, allExtras, allCustomInputs }: Props) {
const router = useRouter();
const form = useForm>({
resolver: zodResolver(commissionTypeSchema),
@@ -44,6 +47,13 @@ export default function EditTypeForm({ type, allOptions, allExtras }: Props) {
pricePercent: e.pricePercent ?? undefined,
priceRange: e.priceRange ?? undefined,
})),
+ customInputs: type.customInputs.map((f) => ({
+ fieldId: f.customInputId,
+ fieldType: f.inputType,
+ label: f.label,
+ name: f.customInput.name,
+ required: f.required,
+ })),
},
})
@@ -93,6 +103,7 @@ export default function EditTypeForm({ type, allOptions, allExtras }: Props) {
+
diff --git a/src/components/items/commissions/types/ListTypes.tsx b/src/components/items/commissions/types/ListTypes.tsx
index 037b39c..904f516 100644
--- a/src/components/items/commissions/types/ListTypes.tsx
+++ b/src/components/items/commissions/types/ListTypes.tsx
@@ -6,7 +6,7 @@ import SortableItemCard from "@/components/drag/SortableItemCard";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
-import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma";
+import { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionType, CommissionTypeCustomInput, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma";
import {
closestCenter,
DndContext,
@@ -30,6 +30,9 @@ type CommissionTypeWithItems = CommissionType & {
})[]
extras: (CommissionTypeExtra & {
extra: CommissionExtra | null
+ })[],
+ customInputs: (CommissionTypeCustomInput & {
+ customInput: CommissionCustomInput
})[]
}
@@ -122,6 +125,16 @@ export default function ListTypes({ types }: { types: CommissionTypeWithItems[]
))}
+
+
Custom Inputs
+
+ {type.customInputs.map((ci) => (
+ -
+ {ci.label}: {ci.inputType}
+
+ ))}
+
+
>({
resolver: zodResolver(commissionTypeSchema),
@@ -78,6 +80,7 @@ export default function NewTypeForm({ options, extras }: Props) {
+
diff --git a/src/components/items/commissions/types/form/CommissionCustomInputField.tsx b/src/components/items/commissions/types/form/CommissionCustomInputField.tsx
new file mode 100644
index 0000000..9d0e22c
--- /dev/null
+++ b/src/components/items/commissions/types/form/CommissionCustomInputField.tsx
@@ -0,0 +1,205 @@
+"use client"
+
+import { createCommissionCustomInput } from "@/actions/items/commissions/types/newType"
+import SortableItem from "@/components/drag/SortableItem"
+import { Button } from "@/components/ui/button"
+import {
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+} from "@/components/ui/form"
+import { Input } from "@/components/ui/input"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Switch } from "@/components/ui/switch"
+import { CommissionCustomInput } from "@/generated/prisma"
+import {
+ closestCenter,
+ DndContext,
+ DragEndEvent,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from "@dnd-kit/core"
+import {
+ SortableContext,
+ verticalListSortingStrategy,
+} from "@dnd-kit/sortable"
+import { useEffect, useState } from "react"
+import { useFieldArray, useFormContext } from "react-hook-form"
+import { ComboboxCreateable } from "./ComboboxCreateable"
+
+type Props = {
+ customInputs: CommissionCustomInput[]
+}
+
+export function CommissionCustomInputField({ customInputs: initialInputs }: Props) {
+ const [mounted, setMounted] = useState(false)
+ const { control, setValue } = useFormContext()
+ const { fields, append, remove, move } = useFieldArray({
+ control,
+ name: "customInputs",
+ })
+
+ const [customInputs, setCustomInputs] = useState(initialInputs)
+ const sensors = useSensors(useSensor(PointerSensor))
+
+ useEffect(() => {
+ setMounted(true)
+ }, [])
+
+ 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)
+ }
+ }
+
+ if (!mounted) return null
+
+ return (
+
+
Custom Inputs
+
+
+ f.id)} strategy={verticalListSortingStrategy}>
+ {fields.map((field, index) => {
+ return (
+
+
+
+ {/* Picker */}
+ (
+
+ Input
+
+ ({
+ label: ci.name,
+ value: ci.id,
+ }))}
+ selected={inputField.value}
+ onSelect={(val) => {
+ const selected = customInputs.find((ci) => ci.id === val)
+ inputField.onChange(val)
+ if (selected) {
+ setValue(`customInputs.${index}.label`, selected.name)
+ setValue(`customInputs.${index}.inputType`, "text")
+ setValue(`customInputs.${index}.required`, false)
+ }
+ }}
+ onCreateNew={async (name) => {
+ const slug = name.toLowerCase().replace(/\s+/g, "-")
+ const newInput = await createCommissionCustomInput({
+ name,
+ fieldId: slug,
+ })
+ setCustomInputs((prev) => [...prev, newInput])
+ inputField.onChange(newInput.id)
+
+ setValue(`customInputs.${index}.label`, newInput.name)
+ setValue(`customInputs.${index}.inputType`, "text")
+ setValue(`customInputs.${index}.required`, false)
+ }}
+ />
+
+
+ )}
+ />
+
+ {/* Label */}
+ (
+
+ Label
+
+
+
+
+ )}
+ />
+
+ {/* Input Type */}
+ (
+
+ Input Type
+
+
+ )}
+ />
+
+ {/* Required */}
+ (
+
+ Required
+
+
+
+
+ )}
+ />
+
+ {/* Remove */}
+
+
+
+ )
+ })}
+
+
+
+
+
+ )
+}
diff --git a/src/schemas/commissionType.ts b/src/schemas/commissionType.ts
index 97dcabc..81008c1 100644
--- a/src/schemas/commissionType.ts
+++ b/src/schemas/commissionType.ts
@@ -16,11 +16,19 @@ const extraField = z.object({
priceRange: z.string().regex(rangePattern, "Format must be like '10–80'").optional(),
});
+const customInputsField = z.object({
+ customInputId: z.string(),
+ inputType: z.string(),
+ label: z.string(),
+ required: z.boolean(),
+});
+
export const commissionTypeSchema = z.object({
name: z.string().min(1, "Name is required. Min 1 character."),
description: z.string().optional(),
options: z.array(optionField).optional(),
extras: z.array(extraField).optional(),
+ customInputs: z.array(customInputsField).optional(),
})
export type commissionTypeSchema = z.infer
\ No newline at end of file