Add custom YCH typs for commission page
This commit is contained in:
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 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<z.infer<typeof commissionOrderSchema>>({
|
||||
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 ? (
|
||||
<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
|
||||
control={form.control}
|
||||
@ -159,7 +257,7 @@ export function CommissionOrderForm({ types }: Props) {
|
||||
<FormLabel>Base Option</FormLabel>
|
||||
<FormControl>
|
||||
<div className="space-y-1">
|
||||
{selectedType.options.map((opt) => (
|
||||
{selection.options.map((opt) => (
|
||||
<label key={opt.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
@ -186,7 +284,7 @@ export function CommissionOrderForm({ types }: Props) {
|
||||
<FormLabel>Extras</FormLabel>
|
||||
<FormControl>
|
||||
<div className="space-y-1">
|
||||
{selectedType.extras.map((ext) => (
|
||||
{selection.extras.map((ext) => (
|
||||
<label key={ext.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
Reference in New Issue
Block a user