Vitron-Front/app/components/store/ProductVariantDrawer.tsx
2026-04-29 01:44:16 +03:30

536 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};