diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2c79f4d..e896983 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -271,6 +271,26 @@ model CommissionType { 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 { id String @id @default(cuid()) createdAt DateTime @default(now()) @@ -282,6 +302,7 @@ model CommissionOption { description String? types CommissionTypeOption[] + customCards CommissionCustomCardOption[] requests CommissionRequest[] } @@ -316,6 +337,7 @@ model CommissionExtra { requests CommissionRequest[] types CommissionTypeExtra[] + customCards CommissionCustomCardExtra[] } model CommissionTypeExtra { @@ -337,6 +359,25 @@ model CommissionTypeExtra { @@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 { id String @id @default(cuid()) createdAt DateTime @default(now()) @@ -368,6 +409,25 @@ model CommissionTypeCustomInput { @@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 { id String @id @default(cuid()) index Int @default(autoincrement()) @@ -386,8 +446,10 @@ model CommissionRequest { optionId String? typeId String? + customCardId String? option CommissionOption? @relation(fields: [optionId], references: [id]) type CommissionType? @relation(fields: [typeId], references: [id]) + customCard CommissionCustomCard? @relation(fields: [customCardId], references: [id]) extras CommissionExtra[] files CommissionRequestFile[] diff --git a/src/actions/commissions/submitCommissionRequest.ts b/src/actions/commissions/submitCommissionRequest.ts index adf373e..79fff4e 100644 --- a/src/actions/commissions/submitCommissionRequest.ts +++ b/src/actions/commissions/submitCommissionRequest.ts @@ -4,6 +4,7 @@ import { z } from "zod"; const submitPayloadSchema = z.object({ typeId: z.string().optional().nullable(), + customCardId: z.string().optional().nullable(), optionId: z.string().optional().nullable(), extraIds: z.array(z.string()).default([]), @@ -11,6 +12,23 @@ const submitPayloadSchema = z.object({ customerEmail: z.string().email(), customerSocials: z.string().optional().nullable(), 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; diff --git a/src/app/(normal)/commissions/page.tsx b/src/app/(normal)/commissions/page.tsx index 65ff2a7..b6f0c4e 100644 --- a/src/app/(normal)/commissions/page.tsx +++ b/src/app/(normal)/commissions/page.tsx @@ -1,4 +1,5 @@ import { CommissionCard } from "@/components/commissions/CommissionCard"; +import { CommissionCustomCard } from "@/components/commissions/CommissionCustomCard"; import CommissionGuidelines from "@/components/commissions/CommissionGuidelines"; import { CommissionOrderForm } from "@/components/commissions/CommissionOrderForm"; import { Button } from "@/components/ui/button"; @@ -13,7 +14,7 @@ import { prisma } from "@/lib/prisma"; import Image from "next/image"; export default async function CommissionsPage() { - const [commissions, guidelines] = await Promise.all([ + const [commissions, customCards, guidelines] = await Promise.all([ prisma.commissionType.findMany({ include: { options: { include: { option: true }, orderBy: { sortIndex: "asc" } }, @@ -22,6 +23,14 @@ export default async function CommissionsPage() { }, 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({ where: { isActive: true }, orderBy: { createdAt: "desc" }, @@ -60,11 +69,14 @@ export default async function CommissionsPage() { {commissions.map((commission) => ( ))} + {customCards.map((card) => ( + + ))}

Request a Commission

- + ); } diff --git a/src/components/commissions/CommissionCustomCard.tsx b/src/components/commissions/CommissionCustomCard.tsx new file mode 100644 index 0000000..513b02a --- /dev/null +++ b/src/components/commissions/CommissionCustomCard.tsx @@ -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 ( +
+ + {card.isSpecialOffer ? ( +
+
+ + Special + +
+
+ ) : null} + + {card.name} +

{card.description}

+ {card.referenceImageUrl ? ( + + + + + + + {card.name} reference + +
+ {`${card.name} +
+
+
+ ) : null} +
+ + +
+

Options

+
    + {card.options.map((option) => ( +
  • + {option.option?.name}:{" "} + {option.price && option.price !== 0 + ? `${option.price}€` + : option.pricePercent + ? `+${option.pricePercent}%` + : option.priceRange && option.priceRange !== "0–0" + ? `${option.priceRange}€` + : "Included"} +
  • + ))} +
+
+ +
+ {card.extras.length > 0 ? ( +

Extras

+ ) : null} +
    + {card.extras.map((extra) => ( +
  • + {extra.extra?.name}:{" "} + {extra.price && extra.price !== 0 + ? `${extra.price}€` + : extra.pricePercent + ? `+${extra.pricePercent}%` + : extra.priceRange && extra.priceRange !== "0–0" + ? `${extra.priceRange}€` + : "Included"} +
  • + ))} +
+
+
+
+
+ ); +} diff --git a/src/components/commissions/CommissionOrderForm.tsx b/src/components/commissions/CommissionOrderForm.tsx index d6622bb..4ff1d32 100644 --- a/src/components/commissions/CommissionOrderForm.tsx +++ b/src/components/commissions/CommissionOrderForm.tsx @@ -14,6 +14,9 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import type { CommissionCustomInput, + CommissionCustomCard, + CommissionCustomCardExtra, + CommissionCustomCardOption, CommissionExtra, CommissionOption, CommissionType, @@ -37,15 +40,40 @@ type CommissionTypeWithRelations = CommissionType & { customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[]; }; -type Props = { - types: CommissionTypeWithRelations[]; +type CommissionCustomCardWithRelations = CommissionCustomCard & { + 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>({ resolver: zodResolver(commissionOrderSchema), defaultValues: { typeId: "", + customCardId: "", optionId: "", extraIds: [], customerName: "", @@ -59,19 +87,49 @@ export function CommissionOrderForm({ types }: Props) { const [isSubmitting, setIsSubmitting] = useState(false); 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 extraIds = useWatch({ control: form.control, name: "extraIds" }); 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( - () => selectedType?.options.find((o) => o.optionId === optionId), - [selectedType, optionId] + () => selection?.options.find((o) => o.optionId === optionId), + [selection, optionId] ); const selectedExtras = useMemo( - () => selectedType?.extras.filter((e) => extraIds?.includes(e.extraId)) ?? [], - [selectedType, extraIds] + () => selection?.extras.filter((e) => extraIds?.includes(e.extraId)) ?? [], + [selection, extraIds] ); const [minPrice, maxPrice] = useMemo(() => { @@ -84,6 +142,7 @@ export function CommissionOrderForm({ types }: Props) { try { const payload = { typeId: values.typeId || null, + customCardId: values.customCardId || null, optionId: values.optionId || null, extraIds: values.extraIds ?? [], customerName: values.customerName, @@ -100,6 +159,7 @@ export function CommissionOrderForm({ types }: Props) { form.reset({ typeId: "", + customCardId: "", optionId: "", extraIds: [], customerName: "", @@ -136,7 +196,12 @@ export function CommissionOrderForm({ types }: Props) { key={type.id} type="button" 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} > {type.name} @@ -149,7 +214,40 @@ export function CommissionOrderForm({ types }: Props) { )} /> - {selectedType && ( + {customCards.length > 0 ? ( + ( + + Custom requests / YCH + +
+ {customCards.map((card) => ( + + ))} +
+
+ +
+ )} + /> + ) : null} + + {selection && ( <> Base Option
- {selectedType.options.map((opt) => ( + {selection.options.map((opt) => (