Add custom YCH typs for commission page
This commit is contained in:
@ -271,6 +271,26 @@ model CommissionType {
|
|||||||
requests CommissionRequest[]
|
requests CommissionRequest[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model CommissionCustomCard {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String
|
||||||
|
|
||||||
|
description String?
|
||||||
|
referenceImageUrl String?
|
||||||
|
isVisible Boolean @default(true)
|
||||||
|
isSpecialOffer Boolean @default(false)
|
||||||
|
|
||||||
|
options CommissionCustomCardOption[]
|
||||||
|
extras CommissionCustomCardExtra[]
|
||||||
|
requests CommissionRequest[]
|
||||||
|
|
||||||
|
@@index([isVisible, sortIndex])
|
||||||
|
}
|
||||||
|
|
||||||
model CommissionOption {
|
model CommissionOption {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@ -282,6 +302,7 @@ model CommissionOption {
|
|||||||
description String?
|
description String?
|
||||||
|
|
||||||
types CommissionTypeOption[]
|
types CommissionTypeOption[]
|
||||||
|
customCards CommissionCustomCardOption[]
|
||||||
requests CommissionRequest[]
|
requests CommissionRequest[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,6 +337,7 @@ model CommissionExtra {
|
|||||||
|
|
||||||
requests CommissionRequest[]
|
requests CommissionRequest[]
|
||||||
types CommissionTypeExtra[]
|
types CommissionTypeExtra[]
|
||||||
|
customCards CommissionCustomCardExtra[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model CommissionTypeExtra {
|
model CommissionTypeExtra {
|
||||||
@ -337,6 +359,25 @@ model CommissionTypeExtra {
|
|||||||
@@unique([typeId, extraId])
|
@@unique([typeId, extraId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model CommissionCustomCardOption {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
cardId String
|
||||||
|
optionId String
|
||||||
|
|
||||||
|
priceRange String?
|
||||||
|
pricePercent Float?
|
||||||
|
price Float?
|
||||||
|
|
||||||
|
card CommissionCustomCard @relation(fields: [cardId], references: [id], onDelete: Cascade)
|
||||||
|
option CommissionOption @relation(fields: [optionId], references: [id])
|
||||||
|
|
||||||
|
@@unique([cardId, optionId])
|
||||||
|
}
|
||||||
|
|
||||||
model CommissionCustomInput {
|
model CommissionCustomInput {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@ -368,6 +409,25 @@ model CommissionTypeCustomInput {
|
|||||||
@@unique([typeId, customInputId])
|
@@unique([typeId, customInputId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model CommissionCustomCardExtra {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
cardId String
|
||||||
|
extraId String
|
||||||
|
|
||||||
|
priceRange String?
|
||||||
|
pricePercent Float?
|
||||||
|
price Float?
|
||||||
|
|
||||||
|
card CommissionCustomCard @relation(fields: [cardId], references: [id], onDelete: Cascade)
|
||||||
|
extra CommissionExtra @relation(fields: [extraId], references: [id])
|
||||||
|
|
||||||
|
@@unique([cardId, extraId])
|
||||||
|
}
|
||||||
|
|
||||||
model CommissionRequest {
|
model CommissionRequest {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
index Int @default(autoincrement())
|
index Int @default(autoincrement())
|
||||||
@ -386,8 +446,10 @@ model CommissionRequest {
|
|||||||
|
|
||||||
optionId String?
|
optionId String?
|
||||||
typeId String?
|
typeId String?
|
||||||
|
customCardId String?
|
||||||
option CommissionOption? @relation(fields: [optionId], references: [id])
|
option CommissionOption? @relation(fields: [optionId], references: [id])
|
||||||
type CommissionType? @relation(fields: [typeId], references: [id])
|
type CommissionType? @relation(fields: [typeId], references: [id])
|
||||||
|
customCard CommissionCustomCard? @relation(fields: [customCardId], references: [id])
|
||||||
|
|
||||||
extras CommissionExtra[]
|
extras CommissionExtra[]
|
||||||
files CommissionRequestFile[]
|
files CommissionRequestFile[]
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const submitPayloadSchema = z.object({
|
const submitPayloadSchema = z.object({
|
||||||
typeId: z.string().optional().nullable(),
|
typeId: z.string().optional().nullable(),
|
||||||
|
customCardId: z.string().optional().nullable(),
|
||||||
optionId: z.string().optional().nullable(),
|
optionId: z.string().optional().nullable(),
|
||||||
extraIds: z.array(z.string()).default([]),
|
extraIds: z.array(z.string()).default([]),
|
||||||
|
|
||||||
@ -11,6 +12,23 @@ const submitPayloadSchema = z.object({
|
|||||||
customerEmail: z.string().email(),
|
customerEmail: z.string().email(),
|
||||||
customerSocials: z.string().optional().nullable(),
|
customerSocials: z.string().optional().nullable(),
|
||||||
message: z.string().min(1),
|
message: z.string().min(1),
|
||||||
|
}).superRefine((data, ctx) => {
|
||||||
|
const hasType = Boolean(data.typeId);
|
||||||
|
const hasCustom = Boolean(data.customCardId);
|
||||||
|
if (!hasType && !hasCustom) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["typeId"],
|
||||||
|
message: "Missing commission type or custom card",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (hasType && hasCustom) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["typeId"],
|
||||||
|
message: "Only one of typeId or customCardId is allowed",
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SubmitCommissionPayload = z.infer<typeof submitPayloadSchema>;
|
export type SubmitCommissionPayload = z.infer<typeof submitPayloadSchema>;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { CommissionCard } from "@/components/commissions/CommissionCard";
|
import { CommissionCard } from "@/components/commissions/CommissionCard";
|
||||||
|
import { CommissionCustomCard } from "@/components/commissions/CommissionCustomCard";
|
||||||
import CommissionGuidelines from "@/components/commissions/CommissionGuidelines";
|
import CommissionGuidelines from "@/components/commissions/CommissionGuidelines";
|
||||||
import { CommissionOrderForm } from "@/components/commissions/CommissionOrderForm";
|
import { CommissionOrderForm } from "@/components/commissions/CommissionOrderForm";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -13,7 +14,7 @@ import { prisma } from "@/lib/prisma";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
export default async function CommissionsPage() {
|
export default async function CommissionsPage() {
|
||||||
const [commissions, guidelines] = await Promise.all([
|
const [commissions, customCards, guidelines] = await Promise.all([
|
||||||
prisma.commissionType.findMany({
|
prisma.commissionType.findMany({
|
||||||
include: {
|
include: {
|
||||||
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
|
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
|
||||||
@ -22,6 +23,14 @@ export default async function CommissionsPage() {
|
|||||||
},
|
},
|
||||||
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
}),
|
}),
|
||||||
|
prisma.commissionCustomCard.findMany({
|
||||||
|
where: { isVisible: true },
|
||||||
|
include: {
|
||||||
|
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
|
||||||
|
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
|
||||||
|
},
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
}),
|
||||||
prisma.commissionGuidelines.findFirst({
|
prisma.commissionGuidelines.findFirst({
|
||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
@ -60,11 +69,14 @@ export default async function CommissionsPage() {
|
|||||||
{commissions.map((commission) => (
|
{commissions.map((commission) => (
|
||||||
<CommissionCard key={commission.id} commission={commission} />
|
<CommissionCard key={commission.id} commission={commission} />
|
||||||
))}
|
))}
|
||||||
|
{customCards.map((card) => (
|
||||||
|
<CommissionCustomCard key={card.id} card={card} />
|
||||||
|
))}
|
||||||
<CommissionGuidelines />
|
<CommissionGuidelines />
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 className="text-2xl font-semibold">Request a Commission</h2>
|
<h2 className="text-2xl font-semibold">Request a Commission</h2>
|
||||||
<CommissionOrderForm types={commissions} />
|
<CommissionOrderForm types={commissions} customCards={customCards} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
140
src/components/commissions/CommissionCustomCard.tsx
Normal file
140
src/components/commissions/CommissionCustomCard.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
type CustomCardOption = {
|
||||||
|
id: string;
|
||||||
|
price: number | null;
|
||||||
|
pricePercent: number | null;
|
||||||
|
priceRange: string | null;
|
||||||
|
option: { name: string } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CustomCardExtra = {
|
||||||
|
id: string;
|
||||||
|
price: number | null;
|
||||||
|
pricePercent: number | null;
|
||||||
|
priceRange: string | null;
|
||||||
|
extra: { name: string } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CommissionCustomCardWithItems = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
referenceImageUrl: string | null;
|
||||||
|
isSpecialOffer: boolean;
|
||||||
|
options: CustomCardOption[];
|
||||||
|
extras: CustomCardExtra[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CommissionCustomCard({
|
||||||
|
card,
|
||||||
|
}: {
|
||||||
|
card: CommissionCustomCardWithItems;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<Card className="flex flex-col flex-1 relative overflow-hidden border-2 border-primary/50 shadow-sm">
|
||||||
|
{card.isSpecialOffer ? (
|
||||||
|
<div className="pointer-events-none absolute right-0 top-0 z-10">
|
||||||
|
<div className="absolute right-0 top-16 h-7 w-36 origin-top-right translate-x-10 rotate-45 bg-primary text-primary-foreground shadow-md">
|
||||||
|
<span className="flex h-full w-full items-center justify-center text-xs font-semibold uppercase tracking-wide">
|
||||||
|
Special
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<CardHeader className="gap-2">
|
||||||
|
<CardTitle className="text-xl font-bold">{card.name}</CardTitle>
|
||||||
|
<p className="text-muted-foreground text-sm">{card.description}</p>
|
||||||
|
{card.referenceImageUrl ? (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="group relative overflow-hidden rounded-lg border border-border/60 bg-muted/40"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={card.referenceImageUrl}
|
||||||
|
alt={`${card.name} reference`}
|
||||||
|
width={800}
|
||||||
|
height={600}
|
||||||
|
sizes="(max-width: 768px) 90vw, 400px"
|
||||||
|
className="h-auto w-full object-cover transition-transform duration-200 group-hover:scale-[1.02]"
|
||||||
|
/>
|
||||||
|
<span className="absolute inset-x-0 bottom-0 bg-background/70 px-2 py-1 text-xs text-foreground/80">
|
||||||
|
Click to enlarge
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="flex w-auto! max-w-[95vw]! flex-col p-4 sm:p-6">
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{card.name} reference</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex max-h-[85vh] max-w-[85vw] items-center justify-center rounded-xl border-border/60 bg-muted p-2 shadow-2xl">
|
||||||
|
<Image
|
||||||
|
src={card.referenceImageUrl}
|
||||||
|
alt={`${card.name} reference`}
|
||||||
|
width={1600}
|
||||||
|
height={1200}
|
||||||
|
sizes="85vw"
|
||||||
|
className="h-auto max-h-[85vh] w-auto max-w-[85vw] rounded-lg object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
) : null}
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex flex-col justify-start gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold">Options</h4>
|
||||||
|
<ul className="pl-4 list-disc">
|
||||||
|
{card.options.map((option) => (
|
||||||
|
<li key={option.id}>
|
||||||
|
{option.option?.name}:{" "}
|
||||||
|
{option.price && option.price !== 0
|
||||||
|
? `${option.price}€`
|
||||||
|
: option.pricePercent
|
||||||
|
? `+${option.pricePercent}%`
|
||||||
|
: option.priceRange && option.priceRange !== "0–0"
|
||||||
|
? `${option.priceRange}€`
|
||||||
|
: "Included"}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{card.extras.length > 0 ? (
|
||||||
|
<h4 className="font-semibold">Extras</h4>
|
||||||
|
) : null}
|
||||||
|
<ul className="pl-4 list-disc">
|
||||||
|
{card.extras.map((extra) => (
|
||||||
|
<li key={extra.id}>
|
||||||
|
{extra.extra?.name}:{" "}
|
||||||
|
{extra.price && extra.price !== 0
|
||||||
|
? `${extra.price}€`
|
||||||
|
: extra.pricePercent
|
||||||
|
? `+${extra.pricePercent}%`
|
||||||
|
: extra.priceRange && extra.priceRange !== "0–0"
|
||||||
|
? `${extra.priceRange}€`
|
||||||
|
: "Included"}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -14,6 +14,9 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import type {
|
import type {
|
||||||
CommissionCustomInput,
|
CommissionCustomInput,
|
||||||
|
CommissionCustomCard,
|
||||||
|
CommissionCustomCardExtra,
|
||||||
|
CommissionCustomCardOption,
|
||||||
CommissionExtra,
|
CommissionExtra,
|
||||||
CommissionOption,
|
CommissionOption,
|
||||||
CommissionType,
|
CommissionType,
|
||||||
@ -37,15 +40,40 @@ type CommissionTypeWithRelations = CommissionType & {
|
|||||||
customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[];
|
customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type CommissionCustomCardWithRelations = CommissionCustomCard & {
|
||||||
types: CommissionTypeWithRelations[];
|
options: (CommissionCustomCardOption & { option: CommissionOption })[];
|
||||||
|
extras: (CommissionCustomCardExtra & { extra: CommissionExtra })[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CommissionOrderForm({ types }: Props) {
|
type Props = {
|
||||||
|
types: CommissionTypeWithRelations[];
|
||||||
|
customCards: CommissionCustomCardWithRelations[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectedOption = {
|
||||||
|
id: string;
|
||||||
|
optionId: string;
|
||||||
|
option: CommissionOption;
|
||||||
|
price: number | null;
|
||||||
|
pricePercent: number | null;
|
||||||
|
priceRange: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectedExtra = {
|
||||||
|
id: string;
|
||||||
|
extraId: string;
|
||||||
|
extra: CommissionExtra;
|
||||||
|
price: number | null;
|
||||||
|
pricePercent: number | null;
|
||||||
|
priceRange: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CommissionOrderForm({ types, customCards }: Props) {
|
||||||
const form = useForm<z.infer<typeof commissionOrderSchema>>({
|
const form = useForm<z.infer<typeof commissionOrderSchema>>({
|
||||||
resolver: zodResolver(commissionOrderSchema),
|
resolver: zodResolver(commissionOrderSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
typeId: "",
|
typeId: "",
|
||||||
|
customCardId: "",
|
||||||
optionId: "",
|
optionId: "",
|
||||||
extraIds: [],
|
extraIds: [],
|
||||||
customerName: "",
|
customerName: "",
|
||||||
@ -59,19 +87,49 @@ export function CommissionOrderForm({ types }: Props) {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const typeId = useWatch({ control: form.control, name: "typeId" });
|
const typeId = useWatch({ control: form.control, name: "typeId" });
|
||||||
|
const customCardId = useWatch({ control: form.control, name: "customCardId" });
|
||||||
const optionId = useWatch({ control: form.control, name: "optionId" });
|
const optionId = useWatch({ control: form.control, name: "optionId" });
|
||||||
const extraIds = useWatch({ control: form.control, name: "extraIds" });
|
const extraIds = useWatch({ control: form.control, name: "extraIds" });
|
||||||
|
|
||||||
const selectedType = useMemo(() => types.find((t) => t.id === typeId), [types, typeId]);
|
const selectedType = useMemo(() => types.find((t) => t.id === typeId), [types, typeId]);
|
||||||
|
const selectedCustomCard = useMemo(
|
||||||
|
() => customCards.find((c) => c.id === customCardId),
|
||||||
|
[customCards, customCardId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selection = useMemo<{
|
||||||
|
kind: "type" | "custom";
|
||||||
|
name: string;
|
||||||
|
options: SelectedOption[];
|
||||||
|
extras: SelectedExtra[];
|
||||||
|
} | null>(() => {
|
||||||
|
if (selectedCustomCard) {
|
||||||
|
return {
|
||||||
|
kind: "custom",
|
||||||
|
name: selectedCustomCard.name,
|
||||||
|
options: selectedCustomCard.options,
|
||||||
|
extras: selectedCustomCard.extras,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (selectedType) {
|
||||||
|
return {
|
||||||
|
kind: "type",
|
||||||
|
name: selectedType.name,
|
||||||
|
options: selectedType.options,
|
||||||
|
extras: selectedType.extras,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [selectedCustomCard, selectedType]);
|
||||||
|
|
||||||
const selectedOption = useMemo(
|
const selectedOption = useMemo(
|
||||||
() => selectedType?.options.find((o) => o.optionId === optionId),
|
() => selection?.options.find((o) => o.optionId === optionId),
|
||||||
[selectedType, optionId]
|
[selection, optionId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedExtras = useMemo(
|
const selectedExtras = useMemo(
|
||||||
() => selectedType?.extras.filter((e) => extraIds?.includes(e.extraId)) ?? [],
|
() => selection?.extras.filter((e) => extraIds?.includes(e.extraId)) ?? [],
|
||||||
[selectedType, extraIds]
|
[selection, extraIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [minPrice, maxPrice] = useMemo(() => {
|
const [minPrice, maxPrice] = useMemo(() => {
|
||||||
@ -84,6 +142,7 @@ export function CommissionOrderForm({ types }: Props) {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
typeId: values.typeId || null,
|
typeId: values.typeId || null,
|
||||||
|
customCardId: values.customCardId || null,
|
||||||
optionId: values.optionId || null,
|
optionId: values.optionId || null,
|
||||||
extraIds: values.extraIds ?? [],
|
extraIds: values.extraIds ?? [],
|
||||||
customerName: values.customerName,
|
customerName: values.customerName,
|
||||||
@ -100,6 +159,7 @@ export function CommissionOrderForm({ types }: Props) {
|
|||||||
|
|
||||||
form.reset({
|
form.reset({
|
||||||
typeId: "",
|
typeId: "",
|
||||||
|
customCardId: "",
|
||||||
optionId: "",
|
optionId: "",
|
||||||
extraIds: [],
|
extraIds: [],
|
||||||
customerName: "",
|
customerName: "",
|
||||||
@ -136,7 +196,12 @@ export function CommissionOrderForm({ types }: Props) {
|
|||||||
key={type.id}
|
key={type.id}
|
||||||
type="button"
|
type="button"
|
||||||
variant={field.value === type.id ? "default" : "outline"}
|
variant={field.value === type.id ? "default" : "outline"}
|
||||||
onClick={() => field.onChange(type.id)}
|
onClick={() => {
|
||||||
|
field.onChange(type.id);
|
||||||
|
form.setValue("customCardId", "");
|
||||||
|
form.setValue("optionId", "");
|
||||||
|
form.setValue("extraIds", []);
|
||||||
|
}}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{type.name}
|
{type.name}
|
||||||
@ -149,7 +214,40 @@ export function CommissionOrderForm({ types }: Props) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedType && (
|
{customCards.length > 0 ? (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="customCardId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Custom requests / YCH</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{customCards.map((card) => (
|
||||||
|
<Button
|
||||||
|
key={card.id}
|
||||||
|
type="button"
|
||||||
|
variant={field.value === card.id ? "default" : "outline"}
|
||||||
|
onClick={() => {
|
||||||
|
field.onChange(card.id);
|
||||||
|
form.setValue("typeId", "");
|
||||||
|
form.setValue("optionId", "");
|
||||||
|
form.setValue("extraIds", []);
|
||||||
|
}}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{card.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{selection && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@ -159,7 +257,7 @@ export function CommissionOrderForm({ types }: Props) {
|
|||||||
<FormLabel>Base Option</FormLabel>
|
<FormLabel>Base Option</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{selectedType.options.map((opt) => (
|
{selection.options.map((opt) => (
|
||||||
<label key={opt.id} className="flex items-center gap-2">
|
<label key={opt.id} className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -186,7 +284,7 @@ export function CommissionOrderForm({ types }: Props) {
|
|||||||
<FormLabel>Extras</FormLabel>
|
<FormLabel>Extras</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{selectedType.extras.map((ext) => (
|
{selection.extras.map((ext) => (
|
||||||
<label key={ext.id} className="flex items-center gap-2">
|
<label key={ext.id} className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import * as z from "zod/v4"
|
import * as z from "zod/v4"
|
||||||
|
|
||||||
export const commissionOrderSchema = z.object({
|
export const commissionOrderSchema = z.object({
|
||||||
typeId: z.string().min(1, "Please select a type"),
|
typeId: z.string().optional(),
|
||||||
|
customCardId: z.string().optional(),
|
||||||
optionId: z.string().min(1, "Please choose a base option"),
|
optionId: z.string().min(1, "Please choose a base option"),
|
||||||
extraIds: z.array(z.string()).optional(),
|
extraIds: z.array(z.string()).optional(),
|
||||||
customFields: z.record(z.string(), z.unknown()).optional(),
|
customFields: z.record(z.string(), z.unknown()).optional(),
|
||||||
@ -9,4 +10,23 @@ export const commissionOrderSchema = z.object({
|
|||||||
customerEmail: z.email("Invalid email"),
|
customerEmail: z.email("Invalid email"),
|
||||||
customerSocials: z.string().optional(),
|
customerSocials: z.string().optional(),
|
||||||
message: z.string().min(5, "Please describe what you want"),
|
message: z.string().min(5, "Please describe what you want"),
|
||||||
|
}).superRefine((data, ctx) => {
|
||||||
|
const hasType = Boolean(data.typeId && data.typeId.length > 0);
|
||||||
|
const hasCustom = Boolean(data.customCardId && data.customCardId.length > 0);
|
||||||
|
|
||||||
|
if (!hasType && !hasCustom) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["typeId"],
|
||||||
|
message: "Please select a commission type or a custom card",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasType && hasCustom) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["typeId"],
|
||||||
|
message: "Choose either a type or a custom card, not both",
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user