Change product modal

This commit is contained in:
g
2025-02-08 00:13:40 +00:00
parent 1b1ab05877
commit e75af20cff
2 changed files with 279 additions and 60 deletions

View File

@@ -19,25 +19,40 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Product } from "@/models/products";
import { Trash, PlusCircle } from "lucide-react";
import { toast } from "sonner";
interface Category {
_id: string;
name: string;
}
interface PricingTier {
minQuantity: number;
pricePerUnit: number;
_id?: string;
}
interface ProductData {
name: string;
description: string;
unitType: string;
category: string;
pricing: PricingTier[];
image: string | File | null;
}
interface ProductModalProps {
open: boolean;
onClose: () => void;
onSave: (productData: Product) => void;
productData: Product;
onSave: (productData: ProductData) => void;
productData: ProductData;
categories: Category[];
editing: boolean;
handleChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
setProductData: React.Dispatch<React.SetStateAction<Product>>;
handleTieredPricingChange: (
e: ChangeEvent<HTMLInputElement>,
index: number
handleChange: (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => void;
setProductData: React.Dispatch<React.SetStateAction<ProductData>>;
}
export const ProductModal = ({
@@ -48,12 +63,24 @@ export const ProductModal = ({
categories,
editing,
handleChange,
handleTieredPricingChange,
setProductData,
}: ProductModalProps) => {
const [imagePreview, setImagePreview] = useState<string | null>(null);
const [newCategory, setNewCategory] = useState("");
const [imageDimensions, setImageDimensions] = useState({
width: 300,
height: 200,
});
useEffect(() => {
if (productData?.pricing) {
setProductData((prev) => ({
...prev,
tieredPricing: productData.pricing,
}));
}
}, [productData.pricing]);
// Update image preview when product data changes
useEffect(() => {
if (productData.image && typeof productData.image === "string") {
setImagePreview(productData.image);
@@ -63,60 +90,123 @@ export const ProductModal = ({
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const image = new Image();
const objectUrl = URL.createObjectURL(file);
image.onload = () => {
const aspectRatio = image.naturalWidth / image.naturalHeight;
let width = 300;
let height = 200;
if (aspectRatio > 1) {
width = 300;
height = 300 / aspectRatio;
} else {
height = 200;
width = 200 * aspectRatio;
}
setProductData({ ...productData, image: file });
setImagePreview(URL.createObjectURL(file));
setImagePreview(objectUrl);
setImageDimensions({ width, height });
};
image.src = objectUrl;
} else {
setProductData({ ...productData, image: null });
setImagePreview(null);
}
};
// ✅ FIXED: Moved Inside the Component & Used Type Assertion
const handleTieredPricingChangeInternal = (
const handleSave = () => {
onSave(productData);
toast.success(
editing ? "Product updated successfully!" : "Product added successfully!"
);
onClose();
};
const handleTieredPricingChange = (
e: ChangeEvent<HTMLInputElement>,
index: number
) => {
const updatedPricing = [...productData.tieredPricing];
const field = e.target.name as keyof (typeof productData.tieredPricing)[number];
const { name, valueAsNumber } = e.target;
if (!productData.pricing) return;
const updatedPricing = [...productData.pricing];
updatedPricing[index] = {
...updatedPricing[index],
[field]: e.target.valueAsNumber || 0,
[name]: isNaN(valueAsNumber) ? 0 : valueAsNumber, // Ensure valid numbers
};
setProductData({
...productData,
setProductData((prev) => ({
...prev,
tieredPricing: updatedPricing,
});
}));
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px]">
<DialogContent className="max-w-6xl">
<DialogHeader>
<DialogTitle className="text-lg">
<DialogTitle className="text-base">
{editing ? "Edit Product" : "Add Product"}
</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-8 py-4">
<div className="space-y-6">
<div>
<label className="text-sm font-medium">Product Name</label>
<Input name="name" placeholder="Product Name" value={productData.name} onChange={handleChange} />
<Input
name="name"
placeholder="Product Name"
value={productData.name}
onChange={handleChange}
className="text-sm h-9"
/>
</div>
<div className="space-y-2">
{/* Description */}
<div>
<label className="text-sm font-medium">Description</label>
<Textarea name="description" placeholder="Product Description" value={productData.description} onChange={handleChange} rows={3} />
<Textarea
name="description"
placeholder="Product Description"
value={productData.description}
onChange={handleChange}
rows={4}
className="text-sm h-24"
/>
</div>
<div className="space-y-2">
{/* Category and Unit Type */}
<div>
<label className="text-sm font-medium">Category</label>
<Select value={productData.category} onValueChange={(value) => setProductData({ ...productData, category: value })}>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
<div className="flex items-center gap-2">
<Select
value={productData.category || "placeholder"}
onValueChange={(value) =>
setProductData((prev) => ({
...prev,
category: value === "placeholder" ? "" : value,
}))
}
>
<SelectTrigger className="flex-1 h-9 text-sm">
<SelectValue
placeholder={
productData.category === "placeholder"
? "Select category..."
: "Select a category"
}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="placeholder">
Select category...
</SelectItem>
{categories.map((cat) => (
<SelectItem key={cat._id} value={cat._id}>
{cat.name}
@@ -124,25 +214,156 @@ export const ProductModal = ({
))}
</SelectContent>
</Select>
{productData.category === "placeholder" && (
<Button
variant="outline"
size="sm"
className="h-9 text-xs"
onClick={() => {
// Handle adding a category (e.g., open modal, show input field, etc.)
console.log("Open category creation modal");
}}
>
+ Add
</Button>
)}
</div>
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium">Tiered Pricing</h3>
{productData.tieredPricing.map((tier, index) => (
<div key={index} className="flex gap-2">
<Input name="minQuantity" type="number" min="1" placeholder="Quantity" value={tier.minQuantity} onChange={(e) => handleTieredPricingChangeInternal(e, index)} />
<Input name="pricePerUnit" type="number" step="0.01" placeholder="Price" value={tier.pricePerUnit} onChange={(e) => handleTieredPricingChangeInternal(e, index)} />
<div>
<label className="text-sm font-medium">Unit Type</label>
<Select
value={productData.unitType || "placeholder"}
onValueChange={(value) =>
setProductData({
...productData,
unitType: value === "placeholder" ? "" : value,
})
}
>
<SelectTrigger className="h-9 text-sm">
<SelectValue placeholder="Select a unit type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="placeholder" disabled>
Select a unit type
</SelectItem>
<SelectItem value="pcs">Pieces (pcs)</SelectItem>
<SelectItem value="gr">Grams (gr)</SelectItem>
<SelectItem value="kg">Kilograms (kg)</SelectItem>
</SelectContent>
</Select>
</div>
))}
<Button variant="outline" size="sm" onClick={() => setProductData({ ...productData, tieredPricing: [...productData.tieredPricing, { minQuantity: 1, pricePerUnit: 0 }] })}>
+ Add Tier
</div>
{/* Column 2: Image and Tiered Pricing */}
<div className="space-y-6">
{/* Product Image */}
<div>
<label className="text-sm font-medium">Product Image</label>
<div className="relative border rounded-md flex items-center justify-center bg-gray-50 dark:bg-gray-800 w-full h-48">
{imagePreview ? (
<img
src={imagePreview}
alt="Preview"
className="object-contain w-full h-full rounded-md"
/>
) : (
<span className="text-gray-400 text-sm">
No Image Selected
</span>
)}
</div>
<Input
type="file"
accept="image/*"
onChange={handleImageChange}
className="mt-2 h-9 text-sm"
/>
</div>
{/* Tiered Pricing */}
<div>
<h3 className="text-sm font-medium">Tiered Pricing</h3>
{productData.pricing?.length > 0 ? (
productData.pricing.map((tier, index) => (
<div
key={tier._id || index}
className="flex items-center gap-2"
>
<Input
name="minQuantity"
type="number"
min="1"
placeholder="Min Quantity"
value={tier.minQuantity}
onChange={(e) => handleTieredPricingChange(e, index)}
className="h-8 text-sm px-2 flex-1"
/>
<Input
name="pricePerUnit"
type="number"
placeholder="Price per unit"
value={tier.pricePerUnit}
onChange={(e) => handleTieredPricingChange(e, index)}
className="h-8 text-sm px-2 flex-1"
/>
<Button
variant="ghost"
size="icon"
className="text-red-500 hover:bg-red-100"
onClick={() => {
const updatedPricing = [...productData.pricing];
updatedPricing.splice(index, 1);
setProductData({
...productData,
pricing: updatedPricing, // Update `pricing`
});
}}
>
<Trash className="h-5 w-5" />
</Button>
</div>
))
) : (
<p className="text-sm text-gray-500">No pricing tiers added.</p>
)}
<Button
variant="outline"
size="sm"
className="mt-2"
onClick={() =>
setProductData((prev) => ({
...prev,
pricing: [
...(prev.pricing || []),
{ minQuantity: 1, pricePerUnit: 0 },
],
}))
}
>
<PlusCircle className="w-4 h-4 mr-1" />
Add Tier
</Button>
</div>
</div>
</div>
<DialogFooter className="flex justify-end gap-2">
<Button variant="outline" onClick={onClose}>Cancel</Button>
<Button onClick={() => onSave(productData)}>{editing ? "Update Product" : "Create Product"}</Button>
<Button
variant="outline"
size="sm"
className="h-9 text-sm px-4"
onClick={onClose}
>
Cancel
</Button>
<Button size="sm" className="h-9 text-sm px-4" onClick={handleSave}>
{editing ? "Update Product" : "Create Product"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -4,8 +4,6 @@ import { ChangeEvent } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ShippingData } from "@/lib/types";
interface ShippingModalProps {