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