536 lines
19 KiB
TypeScript
536 lines
19 KiB
TypeScript
import { useState, useEffect, useRef } from "react";
|
||
import { Button } from "../ui/button";
|
||
import {
|
||
Drawer,
|
||
DrawerContent,
|
||
DrawerHeader,
|
||
DrawerTitle,
|
||
DrawerFooter,
|
||
} from "../ui/drawer";
|
||
import { toast } from "../../hooks/use-toast";
|
||
import { ColorSelector } from "./ColorSelector";
|
||
import { SizeSelector } from "./SizeSelector";
|
||
import { useColorAttributes } from "~/requestHandler/use-product-hooks";
|
||
import AppInput from "../AppInput";
|
||
|
||
interface ProductVariant {
|
||
color: string;
|
||
size: string;
|
||
quantity: number;
|
||
}
|
||
|
||
interface ProductVariantDrawerProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onSave: (variants: ProductVariant[]) => void;
|
||
initialVariants?: ProductVariant[];
|
||
}
|
||
|
||
export const ProductVariantDrawer = ({
|
||
isOpen,
|
||
onClose,
|
||
onSave,
|
||
initialVariants = [],
|
||
}: ProductVariantDrawerProps) => {
|
||
const { data: colorAttributes } = useColorAttributes();
|
||
const [step, setStep] = useState<"attributes" | "quantities">("attributes");
|
||
const [selectedColors, setSelectedColors] = useState<string[]>([]);
|
||
const [selectedSizes, setSelectedSizes] = useState<string[]>([]);
|
||
const [bulkQuantity, setBulkQuantity] = useState("1");
|
||
const [customQuantities, setCustomQuantities] = useState<
|
||
Record<string, number>
|
||
>({});
|
||
const [colorUnselectedMode, setColorUnselectedMode] = useState(false);
|
||
const [sizeUnselectedMode, setSizeUnselectedMode] = useState(false);
|
||
|
||
const initializationDoneRef = useRef<boolean>(false);
|
||
const prevIsOpenRef = useRef<boolean>(false);
|
||
|
||
// Initialize state from initial variants when drawer opens
|
||
useEffect(() => {
|
||
// Only initialize when drawer opens for the first time (not on re-renders)
|
||
if (isOpen && !prevIsOpenRef.current) {
|
||
if (initialVariants.length > 0) {
|
||
// Check if any variant has unselected values
|
||
const hasUnselectedColor = initialVariants.some(
|
||
(v) => v.color === "unselected"
|
||
);
|
||
const hasUnselectedSize = initialVariants.some(
|
||
(v) => v.size === "unselected"
|
||
);
|
||
|
||
// Set unselected modes
|
||
setColorUnselectedMode(hasUnselectedColor);
|
||
setSizeUnselectedMode(hasUnselectedSize);
|
||
|
||
// Extract unique colors and sizes from initial variants (excluding unselected)
|
||
const colors = [
|
||
...new Set(
|
||
initialVariants
|
||
.map((v) => v.color)
|
||
.filter((c) => c !== "unselected")
|
||
),
|
||
];
|
||
const sizes = [
|
||
...new Set(
|
||
initialVariants.map((v) => v.size).filter((s) => s !== "unselected")
|
||
),
|
||
];
|
||
|
||
setSelectedColors(colors);
|
||
setSelectedSizes(sizes);
|
||
|
||
// Set individual quantities
|
||
const quantities: Record<string, number> = {};
|
||
initialVariants.forEach((variant) => {
|
||
const key = `${variant.color}-${variant.size}`;
|
||
quantities[key] = variant.quantity;
|
||
});
|
||
setCustomQuantities(quantities);
|
||
|
||
// Set bulk quantity to the most common quantity or first quantity
|
||
const quantityValues = initialVariants.map((v) => v.quantity);
|
||
const mostCommonQuantity =
|
||
quantityValues.length > 0 ? quantityValues[0] : 1;
|
||
setBulkQuantity(mostCommonQuantity.toString());
|
||
} else {
|
||
// Reset state when opening without initial variants
|
||
setStep("attributes");
|
||
setSelectedColors([]);
|
||
setSelectedSizes([]);
|
||
setBulkQuantity("1");
|
||
setCustomQuantities({});
|
||
setColorUnselectedMode(false);
|
||
setSizeUnselectedMode(false);
|
||
}
|
||
initializationDoneRef.current = true;
|
||
}
|
||
|
||
// Reset when drawer closes
|
||
if (!isOpen && prevIsOpenRef.current) {
|
||
setStep("attributes");
|
||
setSelectedColors([]);
|
||
setSelectedSizes([]);
|
||
setBulkQuantity("1");
|
||
setCustomQuantities({});
|
||
setColorUnselectedMode(false);
|
||
setSizeUnselectedMode(false);
|
||
initializationDoneRef.current = false;
|
||
}
|
||
|
||
prevIsOpenRef.current = isOpen;
|
||
}, [isOpen]);
|
||
|
||
// Separate effect for initialVariants changes (when drawer is already open)
|
||
useEffect(() => {
|
||
if (isOpen && initializationDoneRef.current && initialVariants.length > 0) {
|
||
// Only update if variants actually changed
|
||
const currentVariantKeys = selectedColors
|
||
.flatMap((color) => selectedSizes.map((size) => `${color}-${size}`))
|
||
.sort();
|
||
|
||
const newVariantKeys = initialVariants
|
||
.map((v) => `${v.color}-${v.size}`)
|
||
.sort();
|
||
|
||
if (
|
||
JSON.stringify(currentVariantKeys) !== JSON.stringify(newVariantKeys)
|
||
) {
|
||
// Check if any variant has unselected values
|
||
const hasUnselectedColor = initialVariants.some(
|
||
(v) => v.color === "unselected"
|
||
);
|
||
const hasUnselectedSize = initialVariants.some(
|
||
(v) => v.size === "unselected"
|
||
);
|
||
|
||
// Set unselected modes
|
||
setColorUnselectedMode(hasUnselectedColor);
|
||
setSizeUnselectedMode(hasUnselectedSize);
|
||
|
||
// Extract unique colors and sizes from initial variants (excluding unselected)
|
||
const colors = [
|
||
...new Set(
|
||
initialVariants
|
||
.map((v) => v.color)
|
||
.filter((c) => c !== "unselected")
|
||
),
|
||
];
|
||
const sizes = [
|
||
...new Set(
|
||
initialVariants.map((v) => v.size).filter((s) => s !== "unselected")
|
||
),
|
||
];
|
||
|
||
setSelectedColors(colors);
|
||
setSelectedSizes(sizes);
|
||
|
||
// Set individual quantities
|
||
const quantities: Record<string, number> = {};
|
||
initialVariants.forEach((variant) => {
|
||
const key = `${variant.color}-${variant.size}`;
|
||
quantities[key] = variant.quantity;
|
||
});
|
||
setCustomQuantities(quantities);
|
||
}
|
||
}
|
||
}, [initialVariants, isOpen]);
|
||
|
||
// Generate all possible variants
|
||
const generateVariants = (): ProductVariant[] => {
|
||
const variants: ProductVariant[] = [];
|
||
|
||
getQuantityVariants().forEach((variant) => {
|
||
const { color, size, key } = variant;
|
||
let quantity: number;
|
||
|
||
if (key in customQuantities) {
|
||
// Use custom quantity if exists
|
||
quantity = customQuantities[key];
|
||
} else {
|
||
// Use bulk quantity for variants without custom quantity
|
||
quantity = parseInt(bulkQuantity) || 0;
|
||
}
|
||
|
||
variants.push({ color, size, quantity });
|
||
});
|
||
|
||
return variants;
|
||
};
|
||
|
||
const toggleColor = (colorValue: string) => {
|
||
setSelectedColors((prev) =>
|
||
prev.includes(colorValue)
|
||
? prev.filter((c) => c !== colorValue)
|
||
: [...prev, colorValue]
|
||
);
|
||
};
|
||
|
||
const removeColor = (colorValue: string) => {
|
||
setSelectedColors((prev) => prev.filter((c) => c !== colorValue));
|
||
};
|
||
|
||
const toggleSize = (size: string) => {
|
||
setSelectedSizes((prev) =>
|
||
prev.includes(size) ? prev.filter((s) => s !== size) : [...prev, size]
|
||
);
|
||
};
|
||
|
||
const removeSize = (size: string) => {
|
||
setSelectedSizes((prev) => prev.filter((s) => s !== size));
|
||
};
|
||
|
||
const handleQuantityChange = (
|
||
color: string,
|
||
size: string,
|
||
quantity: string
|
||
) => {
|
||
const key = `${color}-${size}`;
|
||
// Allow empty string or parse to number (including 0)
|
||
const quantityNumber = quantity === "" ? 0 : parseInt(quantity);
|
||
|
||
setCustomQuantities((prev) => ({
|
||
...prev,
|
||
[key]: isNaN(quantityNumber) ? 0 : quantityNumber,
|
||
}));
|
||
};
|
||
|
||
const applyBulkQuantityToAll = () => {
|
||
const bulkQuantityNumber = parseInt(bulkQuantity) || 0;
|
||
const newQuantities: Record<string, number> = {};
|
||
|
||
getQuantityVariants().forEach((variant) => {
|
||
const { key } = variant;
|
||
newQuantities[key] = bulkQuantityNumber;
|
||
});
|
||
|
||
setCustomQuantities((prev) => ({ ...prev, ...newQuantities }));
|
||
};
|
||
|
||
// Get variants for quantity display based on unselected modes
|
||
const getQuantityVariants = () => {
|
||
const variants: { color: string; size: string; key: string }[] = [];
|
||
|
||
if (colorUnselectedMode && sizeUnselectedMode) {
|
||
// Both unselected - show one general variant
|
||
variants.push({
|
||
color: "unselected",
|
||
size: "unselected",
|
||
key: "unselected-unselected",
|
||
});
|
||
} else if (colorUnselectedMode && !sizeUnselectedMode) {
|
||
// Color unselected, size selected - show variants for each selected size
|
||
selectedSizes.forEach((size) => {
|
||
variants.push({
|
||
color: "unselected",
|
||
size: size,
|
||
key: `unselected-${size}`,
|
||
});
|
||
});
|
||
} else if (!colorUnselectedMode && sizeUnselectedMode) {
|
||
// Size unselected, color selected - show variants for each selected color
|
||
selectedColors.forEach((color) => {
|
||
variants.push({
|
||
color: color,
|
||
size: "unselected",
|
||
key: `${color}-unselected`,
|
||
});
|
||
});
|
||
} else {
|
||
// Normal mode - show all combinations
|
||
selectedColors.forEach((color) => {
|
||
selectedSizes.forEach((size) => {
|
||
variants.push({
|
||
color: color,
|
||
size: size,
|
||
key: `${color}-${size}`,
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
return variants;
|
||
};
|
||
|
||
const handleNext = () => {
|
||
// Check if at least one variant can be created
|
||
if (colorUnselectedMode && sizeUnselectedMode) {
|
||
// Both unselected - always valid
|
||
} else if (colorUnselectedMode && !sizeUnselectedMode) {
|
||
// Color unselected, need at least one size
|
||
if (selectedSizes.length === 0) {
|
||
toast({
|
||
title: "خطا",
|
||
description: "لطفاً حداقل یک سایز انتخاب کنید",
|
||
});
|
||
return;
|
||
}
|
||
} else if (!colorUnselectedMode && sizeUnselectedMode) {
|
||
// Size unselected, need at least one color
|
||
if (selectedColors.length === 0) {
|
||
toast({
|
||
title: "خطا",
|
||
description: "لطفاً حداقل یک رنگ انتخاب کنید",
|
||
});
|
||
return;
|
||
}
|
||
} else {
|
||
// Normal mode - need both color and size
|
||
if (selectedColors.length === 0 || selectedSizes.length === 0) {
|
||
toast({
|
||
title: "خطا",
|
||
description:
|
||
"لطفاً حداقل یک رنگ و یک سایز انتخاب کنید یا حالت بدون رنگ/سایز را فعال کنید",
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
setStep("quantities");
|
||
};
|
||
|
||
const handleSave = () => {
|
||
const variants = generateVariants();
|
||
onSave(variants);
|
||
onClose();
|
||
};
|
||
|
||
const handleClose = () => {
|
||
onClose();
|
||
};
|
||
|
||
return (
|
||
<Drawer open={isOpen} onOpenChange={onClose}>
|
||
<DrawerContent className="h-[90vh]">
|
||
<DrawerHeader>
|
||
<DrawerTitle className="text-center">
|
||
{step === "attributes" ? "انتخاب رنگ و سایز" : "تعیین موجودی"}
|
||
</DrawerTitle>
|
||
</DrawerHeader>
|
||
|
||
<div className="flex-1 overflow-y-auto px-4">
|
||
{step === "attributes" ? (
|
||
<div className="space-y-6">
|
||
{/* Color Selection */}
|
||
<ColorSelector
|
||
selectedColors={selectedColors}
|
||
onToggleColor={toggleColor}
|
||
onRemoveColor={removeColor}
|
||
unselectedMode={colorUnselectedMode}
|
||
onToggleUnselectedMode={setColorUnselectedMode}
|
||
/>
|
||
|
||
{/* Size Selection */}
|
||
<SizeSelector
|
||
selectedSizes={selectedSizes}
|
||
onToggleSize={toggleSize}
|
||
onRemoveSize={removeSize}
|
||
unselectedMode={sizeUnselectedMode}
|
||
onToggleUnselectedMode={setSizeUnselectedMode}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-6">
|
||
{/* Bulk Quantity Setting */}
|
||
<div className="relative overflow-hidden rounded-xl border border-primary/20 bg-gradient-to-br from-primary/5 via-primary/10 to-transparent p-6 shadow-sm">
|
||
<div className="absolute -right-6 -top-6 h-24 w-24 rounded-full bg-primary/10 blur-2xl" />
|
||
<div className="relative space-y-4">
|
||
<div className="flex items-center gap-2 text-right">
|
||
<div className="flex-1">
|
||
<h4 className="font-semibold text-base text-foreground">تعیین موجودی عمومی</h4>
|
||
<p className="text-xs text-muted-foreground mt-1">این مقدار به تمام موارد اعمال میشود</p>
|
||
</div>
|
||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
||
<svg className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<Button
|
||
size="sm"
|
||
onClick={applyBulkQuantityToAll}
|
||
className="flex-1 gap-2 shadow-md hover:shadow-lg transition-all duration-200"
|
||
>
|
||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
اعمال به همه
|
||
</Button>
|
||
<div className="w-24">
|
||
<AppInput
|
||
inputTitle=""
|
||
inputValue={bulkQuantity}
|
||
inputOnChange={(e) => setBulkQuantity(e.target.value)}
|
||
inputPlaceholder="0"
|
||
type="number"
|
||
className="text-center font-semibold text-lg"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Individual Variant Quantities */}
|
||
<div className="space-y-4">
|
||
<h4 className="font-medium text-right">موجودی اختصاصی:</h4>
|
||
<div className="space-y-3 overflow-y-auto">
|
||
{getQuantityVariants().map((variant) => {
|
||
const { color, size, key } = variant;
|
||
const quantity =
|
||
key in customQuantities
|
||
? customQuantities[key]
|
||
: parseInt(bulkQuantity) || 0;
|
||
const colorInfo = colorAttributes?.find(
|
||
(c) => c.value === color
|
||
);
|
||
|
||
// Helper function to get display text
|
||
const getDisplayText = () => {
|
||
if (colorUnselectedMode && sizeUnselectedMode) {
|
||
return "کل موجودی محصول";
|
||
} else if (colorUnselectedMode && !sizeUnselectedMode) {
|
||
return `موجودی سایز ${size}`;
|
||
} else if (!colorUnselectedMode && sizeUnselectedMode) {
|
||
return `موجودی ${colorInfo?.info.persian || color}`;
|
||
} else {
|
||
return null; // Normal mode - will show both color and size
|
||
}
|
||
};
|
||
|
||
const displayText = getDisplayText();
|
||
|
||
return (
|
||
<div
|
||
key={key}
|
||
className="flex items-center justify-between p-3 bg-WHITE3 border rounded-lg"
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
{displayText ? (
|
||
// Special unselected modes
|
||
<span className="text-sm font-medium">
|
||
{displayText}
|
||
</span>
|
||
) : (
|
||
// Normal mode - show color and size
|
||
<>
|
||
<div className="flex items-center gap-2">
|
||
<div
|
||
className="w-6 h-6 rounded border border-GRAY3"
|
||
style={{ backgroundColor: color }}
|
||
/>
|
||
<span className="text-sm">
|
||
{colorInfo?.info.persian || color}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-xs text-GRAY">
|
||
سایز:
|
||
</span>
|
||
<span className="text-sm font-medium">
|
||
{size}
|
||
</span>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
<div className="w-16">
|
||
<AppInput
|
||
inputTitle=""
|
||
inputValue={quantity.toString()}
|
||
inputOnChange={(e) =>
|
||
handleQuantityChange(
|
||
color,
|
||
size,
|
||
e.target.value
|
||
)
|
||
}
|
||
inputPlaceholder="0"
|
||
type="number"
|
||
className="text-center text-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<DrawerFooter>
|
||
<div className="flex gap-3 w-full">
|
||
<Button
|
||
variant="outline"
|
||
size="lg"
|
||
onClick={handleClose}
|
||
className="flex-1"
|
||
>
|
||
لغو
|
||
</Button>
|
||
{step === "attributes" ? (
|
||
<Button size="lg" onClick={handleNext} className="flex-1">
|
||
مرحله بعد
|
||
</Button>
|
||
) : (
|
||
<>
|
||
<Button
|
||
variant="outline"
|
||
size="lg"
|
||
onClick={() => setStep("attributes")}
|
||
className="flex-1"
|
||
>
|
||
بازگشت
|
||
</Button>
|
||
<Button size="lg" onClick={handleSave} className="flex-1">
|
||
ذخیره
|
||
</Button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</DrawerFooter>
|
||
</DrawerContent>
|
||
</Drawer>
|
||
);
|
||
};
|