Add commission types
This commit is contained in:
13
bun.lock
13
bun.lock
@ -6,6 +6,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.954.0",
|
"@aws-sdk/client-s3": "^3.954.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
@ -17,6 +19,7 @@
|
|||||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
@ -173,6 +176,14 @@
|
|||||||
|
|
||||||
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
|
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
|
||||||
|
|
||||||
|
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
|
||||||
|
|
||||||
|
"@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
|
||||||
|
|
||||||
|
"@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="],
|
||||||
|
|
||||||
|
"@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
|
||||||
|
|
||||||
"@electric-sql/pglite": ["@electric-sql/pglite@0.3.2", "", {}, "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w=="],
|
"@electric-sql/pglite": ["@electric-sql/pglite@0.3.2", "", {}, "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w=="],
|
||||||
|
|
||||||
"@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.0.6", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.2" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw=="],
|
"@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.0.6", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.2" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw=="],
|
||||||
@ -369,6 +380,8 @@
|
|||||||
|
|
||||||
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
|
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||||
|
|
||||||
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
|
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
|
||||||
|
|||||||
@ -12,6 +12,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.954.0",
|
"@aws-sdk/client-s3": "^3.954.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
@ -23,6 +25,7 @@
|
|||||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
|||||||
142
prisma/migrations/20251223230758_com_1/migration.sql
Normal file
142
prisma/migrations/20251223230758_com_1/migration.sql
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Commission" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"sortIndex" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
CONSTRAINT "Commission_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CommissionType" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"sortIndex" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionType_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CommissionOption" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"sortIndex" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionOption_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CommissionTypeOption" (
|
||||||
|
"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,
|
||||||
|
"optionId" TEXT NOT NULL,
|
||||||
|
"priceRange" TEXT,
|
||||||
|
"pricePercent" DOUBLE PRECISION,
|
||||||
|
"price" DOUBLE PRECISION,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionTypeOption_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CommissionExtra" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"sortIndex" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionExtra_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CommissionTypeExtra" (
|
||||||
|
"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,
|
||||||
|
"extraId" TEXT NOT NULL,
|
||||||
|
"priceRange" TEXT,
|
||||||
|
"pricePercent" DOUBLE PRECISION,
|
||||||
|
"price" DOUBLE PRECISION,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionTypeExtra_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 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,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"fieldId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
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,
|
||||||
|
"inputType" TEXT NOT NULL,
|
||||||
|
"label" TEXT NOT NULL,
|
||||||
|
"required" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionTypeCustomInput_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CommissionRequest" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"sortIndex" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
CONSTRAINT "CommissionRequest_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CommissionTypeOption_typeId_optionId_key" ON "CommissionTypeOption"("typeId", "optionId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CommissionTypeExtra_typeId_extraId_key" ON "CommissionTypeExtra"("typeId", "extraId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CommissionCustomInput_name_key" ON "CommissionCustomInput"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CommissionTypeCustomInput_typeId_customInputId_key" ON "CommissionTypeCustomInput"("typeId", "customInputId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CommissionTypeOption" ADD CONSTRAINT "CommissionTypeOption_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CommissionType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CommissionTypeOption" ADD CONSTRAINT "CommissionTypeOption_optionId_fkey" FOREIGN KEY ("optionId") REFERENCES "CommissionOption"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CommissionTypeExtra" ADD CONSTRAINT "CommissionTypeExtra_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CommissionType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CommissionTypeExtra" ADD CONSTRAINT "CommissionTypeExtra_extraId_fkey" FOREIGN KEY ("extraId") REFERENCES "CommissionExtra"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- 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;
|
||||||
@ -222,3 +222,127 @@ model FileVariant {
|
|||||||
|
|
||||||
@@unique([artworkId, type])
|
@@unique([artworkId, type])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Commission {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
model CommissionType {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String
|
||||||
|
|
||||||
|
description String?
|
||||||
|
|
||||||
|
options CommissionTypeOption[]
|
||||||
|
extras CommissionTypeExtra[]
|
||||||
|
customInputs CommissionTypeCustomInput[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model CommissionOption {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String
|
||||||
|
|
||||||
|
description String?
|
||||||
|
|
||||||
|
types CommissionTypeOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model CommissionTypeOption {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
typeId String
|
||||||
|
optionId String
|
||||||
|
|
||||||
|
priceRange String?
|
||||||
|
pricePercent Float?
|
||||||
|
price Float?
|
||||||
|
|
||||||
|
type CommissionType @relation(fields: [typeId], references: [id])
|
||||||
|
option CommissionOption @relation(fields: [optionId], references: [id])
|
||||||
|
|
||||||
|
@@unique([typeId, optionId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model CommissionExtra {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String
|
||||||
|
|
||||||
|
description String?
|
||||||
|
|
||||||
|
types CommissionTypeExtra[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model CommissionTypeExtra {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
typeId String
|
||||||
|
extraId String
|
||||||
|
|
||||||
|
priceRange String?
|
||||||
|
pricePercent Float?
|
||||||
|
price Float?
|
||||||
|
|
||||||
|
type CommissionType @relation(fields: [typeId], references: [id])
|
||||||
|
extra CommissionExtra @relation(fields: [extraId], references: [id])
|
||||||
|
|
||||||
|
@@unique([typeId, extraId])
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 CommissionRequest {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
}
|
||||||
|
|||||||
19
src/actions/commissions/types/deleteType.ts
Normal file
19
src/actions/commissions/types/deleteType.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma"
|
||||||
|
|
||||||
|
export async function deleteCommissionType(typeId: string) {
|
||||||
|
|
||||||
|
await prisma.commissionTypeOption.deleteMany({
|
||||||
|
where: { typeId },
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.commissionTypeExtra.deleteMany({
|
||||||
|
where: { typeId },
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.commissionType.delete({
|
||||||
|
where: { id: typeId },
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
81
src/actions/commissions/types/newType.ts
Normal file
81
src/actions/commissions/types/newType.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import { commissionTypeSchema } from "@/schemas/commissionType"
|
||||||
|
|
||||||
|
export async function createCommissionOption(data: { name: string }) {
|
||||||
|
return await prisma.commissionOption.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCommissionExtra(data: { name: string }) {
|
||||||
|
return await prisma.commissionExtra.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
console.error("Validation failed", parsed.error)
|
||||||
|
throw new Error("Invalid input")
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = parsed.data
|
||||||
|
|
||||||
|
const created = await prisma.commissionType.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
options: {
|
||||||
|
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, index) => ({
|
||||||
|
extra: { connect: { id: ext.extraId } },
|
||||||
|
price: ext.price,
|
||||||
|
pricePercent: ext.pricePercent,
|
||||||
|
priceRange: ext.priceRange,
|
||||||
|
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,
|
||||||
|
})) || [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return created
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
57
src/actions/commissions/types/updateType.ts
Normal file
57
src/actions/commissions/types/updateType.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import { commissionTypeSchema } from "@/schemas/commissionType"
|
||||||
|
import * as z from "zod/v4"
|
||||||
|
|
||||||
|
export async function updateCommissionType(
|
||||||
|
id: string,
|
||||||
|
rawData: z.infer<typeof commissionTypeSchema>
|
||||||
|
) {
|
||||||
|
const data = commissionTypeSchema.parse(rawData)
|
||||||
|
|
||||||
|
const updated = await prisma.commissionType.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
options: {
|
||||||
|
deleteMany: {},
|
||||||
|
create: data.options?.map((opt, index) => ({
|
||||||
|
option: { connect: { id: opt.optionId } },
|
||||||
|
price: opt.price ?? null,
|
||||||
|
pricePercent: opt.pricePercent ?? null,
|
||||||
|
priceRange: opt.priceRange ?? null,
|
||||||
|
sortIndex: index,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
extras: {
|
||||||
|
deleteMany: {},
|
||||||
|
create: data.extras?.map((ext, index) => ({
|
||||||
|
extra: { connect: { id: ext.extraId } },
|
||||||
|
price: ext.price ?? null,
|
||||||
|
pricePercent: ext.pricePercent ?? null,
|
||||||
|
priceRange: ext.priceRange ?? null,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return updated
|
||||||
|
}
|
||||||
5
src/app/commissions/page.tsx
Normal file
5
src/app/commissions/page.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default function CommissionPage() {
|
||||||
|
return (
|
||||||
|
<div>CommissionPage</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/app/commissions/types/[id]/page.tsx
Normal file
38
src/app/commissions/types/[id]/page.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import EditTypeForm from "@/components/commissions/types/EditTypeForm";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export default async function CommissionTypesEditPage({ params }: { params: { id: string } }) {
|
||||||
|
const { id } = await params;
|
||||||
|
const commissionType = await prisma.commissionType.findUnique({
|
||||||
|
where: {
|
||||||
|
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({
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
});
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<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} allCustomInputs={customInputs} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/app/commissions/types/new/page.tsx
Normal file
24
src/app/commissions/types/new/page.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import NewTypeForm from "@/components/commissions/types/NewTypeForm";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export default async function CommissionTypesNewPage() {
|
||||||
|
const options = await prisma.commissionOption.findMany({
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
});
|
||||||
|
const extras = await prisma.commissionExtra.findMany({
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
})
|
||||||
|
const customInputs = await prisma.commissionCustomInput.findMany({
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<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} customInputs={customInputs} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/app/commissions/types/page.tsx
Normal file
27
src/app/commissions/types/page.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import ListTypes from "@/components/commissions/types/ListTypes";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { PlusCircleIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default async function CommissionTypesPage() {
|
||||||
|
const types = await prisma.commissionType.findMany({
|
||||||
|
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" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-4 justify-between pb-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Commission Types</h1>
|
||||||
|
<Link href="/commissions/types/new" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
|
||||||
|
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new Type
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{types && types.length > 0 ? <ListTypes types={types} /> : <p className="text-muted-foreground italic">No types found.</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
src/components/commissions/types/EditTypeForm.tsx
Normal file
116
src/components/commissions/types/EditTypeForm.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { updateCommissionType } from "@/actions/commissions/types/updateType";
|
||||||
|
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 { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionType, CommissionTypeCustomInput, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma/client";
|
||||||
|
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, allCustomInputs }: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
const form = useForm<z.infer<typeof commissionTypeSchema>>({
|
||||||
|
resolver: zodResolver(commissionTypeSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: type.name,
|
||||||
|
description: type.description ?? "",
|
||||||
|
options: type.options.map((o) => ({
|
||||||
|
optionId: o.optionId,
|
||||||
|
price: o.price ?? undefined,
|
||||||
|
pricePercent: o.pricePercent ?? undefined,
|
||||||
|
priceRange: o.priceRange ?? undefined,
|
||||||
|
})),
|
||||||
|
extras: type.extras.map((e) => ({
|
||||||
|
extraId: e.extraId,
|
||||||
|
price: e.price ?? undefined,
|
||||||
|
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,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof commissionTypeSchema>) {
|
||||||
|
try {
|
||||||
|
await updateCommissionType(type.id, values)
|
||||||
|
toast.success("Commission type updated.")
|
||||||
|
router.push("/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={allOptions} />
|
||||||
|
<CommissionExtraField extras={allExtras} />
|
||||||
|
<CommissionCustomInputField customInputs={allCustomInputs} />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
190
src/components/commissions/types/ListTypes.tsx
Normal file
190
src/components/commissions/types/ListTypes.tsx
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { deleteCommissionType } from "@/actions/commissions/types/deleteType";
|
||||||
|
import { updateCommissionTypeSortOrder } from "@/actions/commissions/types/updateCommissionTypeSortOrder";
|
||||||
|
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 { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionType, CommissionTypeCustomInput, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma/client";
|
||||||
|
import {
|
||||||
|
closestCenter,
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
arrayMove,
|
||||||
|
rectSortingStrategy,
|
||||||
|
SortableContext
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { PencilIcon, TrashIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState, useTransition } from "react";
|
||||||
|
import SortableItemCard from "./SortableItemCard";
|
||||||
|
|
||||||
|
type CommissionTypeWithItems = CommissionType & {
|
||||||
|
options: (CommissionTypeOption & {
|
||||||
|
option: CommissionOption | null
|
||||||
|
})[]
|
||||||
|
extras: (CommissionTypeExtra & {
|
||||||
|
extra: CommissionExtra | null
|
||||||
|
})[],
|
||||||
|
customInputs: (CommissionTypeCustomInput & {
|
||||||
|
customInput: CommissionCustomInput
|
||||||
|
})[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ListTypes({ types }: { types: CommissionTypeWithItems[] }) {
|
||||||
|
const [items, setItems] = useState(types)
|
||||||
|
const [isMounted, setIsMounted] = useState(false)
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false)
|
||||||
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
|
||||||
|
const [isPending, startTransition] = useTransition()
|
||||||
|
|
||||||
|
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 })))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = () => {
|
||||||
|
if (!deleteTargetId) return
|
||||||
|
startTransition(async () => {
|
||||||
|
await deleteCommissionType(deleteTargetId)
|
||||||
|
setItems((prev) => prev.filter((i) => i.id !== deleteTargetId))
|
||||||
|
setDialogOpen(false)
|
||||||
|
setDeleteTargetId(null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMounted) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<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>
|
||||||
|
<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
|
||||||
|
href={`/commissions/types/${type.id}`}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Button variant="default" className="w-full flex items-center gap-2">
|
||||||
|
<PencilIcon className="h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteTargetId(type.id)
|
||||||
|
setDialogOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</SortableItemCard >
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</div >
|
||||||
|
|
||||||
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete this commission type?</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<p>This action cannot be undone. Are you sure you want to continue?</p>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDialogOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
disabled={isPending}
|
||||||
|
onClick={confirmDelete}
|
||||||
|
>
|
||||||
|
Confirm Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
src/components/commissions/types/NewTypeForm.tsx
Normal file
93
src/components/commissions/types/NewTypeForm.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createCommissionType } from "@/actions/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 { CommissionCustomInput, CommissionExtra, CommissionOption } from "@/generated/prisma/client";
|
||||||
|
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, customInputs }: 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)
|
||||||
|
toast("Commission type created.")
|
||||||
|
router.push("/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} />
|
||||||
|
<CommissionCustomInputField customInputs={customInputs} />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/components/commissions/types/SortableItem.tsx
Normal file
49
src/components/commissions/types/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,
|
||||||
|
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-16">
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
44
src/components/commissions/types/SortableItemCard.tsx
Normal file
44
src/components/commissions/types/SortableItemCard.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"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}>
|
||||||
|
<div
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
className="cursor-grab px-2 py-1 text-sm text-muted-foreground"
|
||||||
|
title="Drag to reorder"
|
||||||
|
>
|
||||||
|
☰
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
111
src/components/commissions/types/form/ComboboxCreateable.tsx
Normal file
111
src/components/commissions/types/form/ComboboxCreateable.tsx
Normal 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-75 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,205 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createCommissionCustomInput } from "@/actions/commissions/types/newType"
|
||||||
|
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/client"
|
||||||
|
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 SortableItem from "../SortableItem"
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
206
src/components/commissions/types/form/CommissionExtraField.tsx
Normal file
206
src/components/commissions/types/form/CommissionExtraField.tsx
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createCommissionExtra } from "@/actions/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/client"
|
||||||
|
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 SortableItem from "../SortableItem"
|
||||||
|
import { ComboboxCreateable } from "./ComboboxCreateable"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
extras: CommissionExtra[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommissionExtraField({ extras: initialExtras }: Props) {
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const { control } = useFormContext()
|
||||||
|
const { fields, append, remove, move } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "extras",
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold">Extras</h3>
|
||||||
|
|
||||||
|
<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 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
|
||||||
|
label={(value) => value}
|
||||||
|
value={[start, end]}
|
||||||
|
onValueChange={([min, max]) =>
|
||||||
|
field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
|
||||||
|
}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Remove Button */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SortableItem>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext >
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
append({
|
||||||
|
extraId: "",
|
||||||
|
price: undefined,
|
||||||
|
pricePercent: undefined,
|
||||||
|
priceRange: "0–0",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Extra
|
||||||
|
</Button>
|
||||||
|
</div >
|
||||||
|
)
|
||||||
|
}
|
||||||
208
src/components/commissions/types/form/CommissionOptionField.tsx
Normal file
208
src/components/commissions/types/form/CommissionOptionField.tsx
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createCommissionOption } from "@/actions/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/client"
|
||||||
|
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 SortableItem from "../SortableItem"
|
||||||
|
import { ComboboxCreateable } from "./ComboboxCreateable"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: CommissionOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommissionOptionField({ options: initialOptions }: Props) {
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const { control } = useFormContext()
|
||||||
|
const { fields, append, remove, move } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "options",
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold">Options</h3>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
{/* 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
|
||||||
|
label={(value) => value}
|
||||||
|
value={[start, end]}
|
||||||
|
onValueChange={([min, max]) =>
|
||||||
|
field.onChange(`${Math.min(min, max)}–${Math.max(min, max)}`)
|
||||||
|
}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Remove Button */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SortableItem>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
|
||||||
|
{/* Add Button */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
append({
|
||||||
|
optionId: "",
|
||||||
|
price: undefined,
|
||||||
|
pricePercent: undefined,
|
||||||
|
priceRange: "0–0",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Option
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -103,6 +103,12 @@ export default function TopNav() {
|
|||||||
</NavigationMenuContent>
|
</NavigationMenuContent>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||||
|
<Link href="/commissions">Commissions</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
|
||||||
{/* <NavigationMenuItem>
|
{/* <NavigationMenuItem>
|
||||||
<NavigationMenuTrigger>Portfolio</NavigationMenuTrigger>
|
<NavigationMenuTrigger>Portfolio</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuContent>
|
||||||
|
|||||||
50
src/components/ui/dual-range.tsx
Normal file
50
src/components/ui/dual-range.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as SliderPrimitive from '@radix-ui/react-slider';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface DualRangeSliderProps extends React.ComponentProps<typeof SliderPrimitive.Root> {
|
||||||
|
labelPosition?: 'top' | 'bottom';
|
||||||
|
label?: (value: number | undefined) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DualRangeSlider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
DualRangeSliderProps
|
||||||
|
>(({ className, label, labelPosition = 'top', ...props }, ref) => {
|
||||||
|
const initialValue = Array.isArray(props.value) ? props.value : [props.min, props.max];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
{initialValue.map((value, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<SliderPrimitive.Thumb className="relative block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50">
|
||||||
|
{label && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'absolute flex w-full justify-center',
|
||||||
|
labelPosition === 'top' && '-top-7',
|
||||||
|
labelPosition === 'bottom' && 'top-4',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label(value)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</SliderPrimitive.Thumb>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
DualRangeSlider.displayName = 'DualRangeSlider';
|
||||||
|
|
||||||
|
export { DualRangeSlider };
|
||||||
63
src/components/ui/slider.tsx
Normal file
63
src/components/ui/slider.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Slider({
|
||||||
|
className,
|
||||||
|
defaultValue,
|
||||||
|
value,
|
||||||
|
min = 0,
|
||||||
|
max = 100,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||||
|
const _values = React.useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: Array.isArray(defaultValue)
|
||||||
|
? defaultValue
|
||||||
|
: [min, max],
|
||||||
|
[value, defaultValue, min, max]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
data-slot="slider"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track
|
||||||
|
data-slot="slider-track"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Range
|
||||||
|
data-slot="slider-range"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
{Array.from({ length: _values.length }, (_, index) => (
|
||||||
|
<SliderPrimitive.Thumb
|
||||||
|
data-slot="slider-thumb"
|
||||||
|
key={index}
|
||||||
|
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Slider }
|
||||||
34
src/schemas/commissionType.ts
Normal file
34
src/schemas/commissionType.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
const rangePattern = /^\d{1,3}–\d{1,3}$/;
|
||||||
|
|
||||||
|
const optionField = z.object({
|
||||||
|
optionId: z.string(),
|
||||||
|
price: z.number().optional(),
|
||||||
|
pricePercent: z.number().optional(),
|
||||||
|
priceRange: z.string().regex(rangePattern, "Format must be like '10–80'").optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const extraField = z.object({
|
||||||
|
extraId: z.string(),
|
||||||
|
price: z.number().optional(),
|
||||||
|
pricePercent: z.number().optional(),
|
||||||
|
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