Add custom inputs to commission types
This commit is contained in:
		
							
								
								
									
										34
									
								
								prisma/migrations/20250707151218_type_field/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								prisma/migrations/20250707151218_type_field/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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");
 | 
			
		||||
@ -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())
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
        })) || [],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 <div>Type not found</div>
 | 
			
		||||
@ -28,7 +32,7 @@ export default async function CommissionTypesEditPage({ params }: { params: { id
 | 
			
		||||
      <div className="flex gap-4 justify-between pb-8">
 | 
			
		||||
        <h1 className="text-2xl font-bold mb-4">Edit Commission Type</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
      <EditTypeForm type={commissionType} allOptions={options} allExtras={extras} />
 | 
			
		||||
      <EditTypeForm type={commissionType} allOptions={options} allExtras={extras} allCustomInputs={customInputs} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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() {
 | 
			
		||||
      <div className="flex gap-4 justify-between pb-8">
 | 
			
		||||
        <h1 className="text-2xl font-bold mb-4">New Commission Type</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
      <NewTypeForm options={options} extras={extras} />
 | 
			
		||||
      <NewTypeForm options={options} extras={extras} customInputs={customInputs} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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" }],
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -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<z.infer<typeof commissionTypeSchema>>({
 | 
			
		||||
    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) {
 | 
			
		||||
 | 
			
		||||
          <CommissionOptionField options={allOptions} />
 | 
			
		||||
          <CommissionExtraField extras={allExtras} />
 | 
			
		||||
          <CommissionCustomInputField customInputs={allCustomInputs} />
 | 
			
		||||
 | 
			
		||||
          <div className="flex flex-col gap-4">
 | 
			
		||||
            <Button type="submit">Submit</Button>
 | 
			
		||||
 | 
			
		||||
@ -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[]
 | 
			
		||||
                        ))}
 | 
			
		||||
                      </ul>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div>
 | 
			
		||||
                      <h4 className="font-semibold">Custom Inputs</h4>
 | 
			
		||||
                      <ul className="pl-4 list-disc">
 | 
			
		||||
                        {type.customInputs.map((ci) => (
 | 
			
		||||
                          <li key={ci.id}>
 | 
			
		||||
                            {ci.label}: {ci.inputType}
 | 
			
		||||
                          </li>
 | 
			
		||||
                        ))}
 | 
			
		||||
                      </ul>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </CardContent>
 | 
			
		||||
                  <CardFooter className="flex flex-col gap-2">
 | 
			
		||||
                    <Link
 | 
			
		||||
 | 
			
		||||
@ -4,22 +4,24 @@ 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 { CommissionCustomInput, 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 { CommissionCustomInputField } from "./form/CommissionCustomInputField";
 | 
			
		||||
import { CommissionExtraField } from "./form/CommissionExtraField";
 | 
			
		||||
import { CommissionOptionField } from "./form/CommissionOptionField";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  options: CommissionOption[],
 | 
			
		||||
  extras: CommissionExtra[],
 | 
			
		||||
  customInputs: CommissionCustomInput[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function NewTypeForm({ options, extras }: Props) {
 | 
			
		||||
export default function NewTypeForm({ options, extras, customInputs }: Props) {
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const form = useForm<z.infer<typeof commissionTypeSchema>>({
 | 
			
		||||
    resolver: zodResolver(commissionTypeSchema),
 | 
			
		||||
@ -78,6 +80,7 @@ export default function NewTypeForm({ options, extras }: Props) {
 | 
			
		||||
 | 
			
		||||
          <CommissionOptionField options={options} />
 | 
			
		||||
          <CommissionExtraField extras={extras} />
 | 
			
		||||
          <CommissionCustomInputField customInputs={customInputs} />
 | 
			
		||||
 | 
			
		||||
          <div className="flex flex-col gap-4">
 | 
			
		||||
            <Button type="submit">Submit</Button>
 | 
			
		||||
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <div className="space-y-4">
 | 
			
		||||
      <h3 className="text-lg font-semibold">Custom Inputs</h3>
 | 
			
		||||
 | 
			
		||||
      <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
 | 
			
		||||
        <SortableContext items={fields.map((f) => f.id)} strategy={verticalListSortingStrategy}>
 | 
			
		||||
          {fields.map((field, index) => {
 | 
			
		||||
            return (
 | 
			
		||||
              <SortableItem key={field.id} id={field.id}>
 | 
			
		||||
                <div className="grid grid-cols-5 gap-4 items-end">
 | 
			
		||||
 | 
			
		||||
                  {/* Picker */}
 | 
			
		||||
                  <FormField
 | 
			
		||||
                    control={control}
 | 
			
		||||
                    name={`customInputs.${index}.customInputId`}
 | 
			
		||||
                    render={({ field: inputField }) => (
 | 
			
		||||
                      <FormItem>
 | 
			
		||||
                        <FormLabel>Input</FormLabel>
 | 
			
		||||
                        <FormControl>
 | 
			
		||||
                          <ComboboxCreateable
 | 
			
		||||
                            options={customInputs.map((ci) => ({
 | 
			
		||||
                              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)
 | 
			
		||||
                            }}
 | 
			
		||||
                          />
 | 
			
		||||
                        </FormControl>
 | 
			
		||||
                      </FormItem>
 | 
			
		||||
                    )}
 | 
			
		||||
                  />
 | 
			
		||||
 | 
			
		||||
                  {/* Label */}
 | 
			
		||||
                  <FormField
 | 
			
		||||
                    control={control}
 | 
			
		||||
                    name={`customInputs.${index}.label`}
 | 
			
		||||
                    render={({ field }) => (
 | 
			
		||||
                      <FormItem>
 | 
			
		||||
                        <FormLabel>Label</FormLabel>
 | 
			
		||||
                        <FormControl>
 | 
			
		||||
                          <Input {...field} />
 | 
			
		||||
                        </FormControl>
 | 
			
		||||
                      </FormItem>
 | 
			
		||||
                    )}
 | 
			
		||||
                  />
 | 
			
		||||
 | 
			
		||||
                  {/* Input Type */}
 | 
			
		||||
                  <FormField
 | 
			
		||||
                    control={control}
 | 
			
		||||
                    name={`customInputs.${index}.inputType`}
 | 
			
		||||
                    render={({ field }) => (
 | 
			
		||||
                      <FormItem>
 | 
			
		||||
                        <FormLabel>Input Type</FormLabel>
 | 
			
		||||
                        <Select onValueChange={field.onChange} defaultValue={field.value}>
 | 
			
		||||
                          <FormControl>
 | 
			
		||||
                            <SelectTrigger>
 | 
			
		||||
                              <SelectValue placeholder="Select input type" />
 | 
			
		||||
                            </SelectTrigger>
 | 
			
		||||
                          </FormControl>
 | 
			
		||||
                          <SelectContent>
 | 
			
		||||
                            <SelectItem value="text">Text</SelectItem>
 | 
			
		||||
                            <SelectItem value="textarea">Textarea</SelectItem>
 | 
			
		||||
                            <SelectItem value="number">Number</SelectItem>
 | 
			
		||||
                            <SelectItem value="checkbox">Checkbox</SelectItem>
 | 
			
		||||
                            <SelectItem value="date">Date</SelectItem>
 | 
			
		||||
                            <SelectItem value="select">Dropdown (Select)</SelectItem>
 | 
			
		||||
                          </SelectContent>
 | 
			
		||||
                        </Select>
 | 
			
		||||
                      </FormItem>
 | 
			
		||||
                    )}
 | 
			
		||||
                  />
 | 
			
		||||
 | 
			
		||||
                  {/* Required */}
 | 
			
		||||
                  <FormField
 | 
			
		||||
                    control={control}
 | 
			
		||||
                    name={`customInputs.${index}.required`}
 | 
			
		||||
                    render={({ field }) => (
 | 
			
		||||
                      <FormItem>
 | 
			
		||||
                        <FormLabel>Required</FormLabel>
 | 
			
		||||
                        <FormControl>
 | 
			
		||||
                          <Switch
 | 
			
		||||
                            checked={field.value}
 | 
			
		||||
                            onCheckedChange={field.onChange}
 | 
			
		||||
                          />
 | 
			
		||||
                        </FormControl>
 | 
			
		||||
                      </FormItem>
 | 
			
		||||
                    )}
 | 
			
		||||
                  />
 | 
			
		||||
 | 
			
		||||
                  {/* Remove */}
 | 
			
		||||
                  <Button type="button" variant="destructive" onClick={() => remove(index)}>
 | 
			
		||||
                    Remove
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </SortableItem>
 | 
			
		||||
            )
 | 
			
		||||
          })}
 | 
			
		||||
        </SortableContext>
 | 
			
		||||
      </DndContext>
 | 
			
		||||
 | 
			
		||||
      <Button
 | 
			
		||||
        type="button"
 | 
			
		||||
        onClick={() =>
 | 
			
		||||
          append({
 | 
			
		||||
            customInputId: "",
 | 
			
		||||
            label: "",
 | 
			
		||||
            inputType: "text",
 | 
			
		||||
            required: false,
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        Add Input
 | 
			
		||||
      </Button>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -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<typeof commissionTypeSchema>
 | 
			
		||||
		Reference in New Issue
	
	Block a user