268 lines
8.5 KiB
TypeScript
268 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import { ChangeEvent, useState, useEffect } 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 { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Product } from "@/models/products";
|
|
|
|
interface Category {
|
|
_id: string;
|
|
name: string;
|
|
}
|
|
|
|
interface ProductModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSave: (productData: Product) => void;
|
|
productData: Product;
|
|
categories: Category[]; // Define categories as an array of Category
|
|
editing: boolean;
|
|
handleChange: (
|
|
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
) => void;
|
|
handleTieredPricingChange: (
|
|
e: ChangeEvent<HTMLInputElement>,
|
|
index: number
|
|
) => void;
|
|
setProductData: React.Dispatch<React.SetStateAction<Product>>;
|
|
}
|
|
|
|
export const ProductModal = ({
|
|
open,
|
|
onClose,
|
|
onSave,
|
|
productData,
|
|
categories,
|
|
editing,
|
|
handleChange,
|
|
handleTieredPricingChange,
|
|
setProductData,
|
|
}: ProductModalProps) => {
|
|
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
|
|
|
// Update image preview when product data changes (for editing purposes)
|
|
useEffect(() => {
|
|
if (productData.image && typeof productData.image === "string") {
|
|
setImagePreview(productData.image); // For existing product image
|
|
}
|
|
}, [productData.image]);
|
|
|
|
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
setProductData({ ...productData, image: file });
|
|
setImagePreview(URL.createObjectURL(file));
|
|
} else {
|
|
setProductData({ ...productData, image: null });
|
|
setImagePreview(null);
|
|
}
|
|
};
|
|
|
|
// Handle tiered pricing change
|
|
const handleTieredPricingChangeInternal = (
|
|
e: ChangeEvent<HTMLInputElement>,
|
|
index: number
|
|
) => {
|
|
const updatedPricing = [...productData.tieredPricing];
|
|
const updatedTier = updatedPricing[index];
|
|
|
|
// Ensure pricePerUnit is a number
|
|
const value = e.target.name === "pricePerUnit"
|
|
? parseFloat(e.target.value) || 0 // If value is invalid, default to 0
|
|
: e.target.value;
|
|
|
|
updatedTier[e.target.name] = value;
|
|
setProductData({ ...productData, tieredPricing: updatedPricing });
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onClose}>
|
|
<DialogContent className="sm:max-w-[600px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-lg">
|
|
{editing ? "Edit Product" : "Add Product"}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="grid gap-4 py-4">
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Product Name</label>
|
|
<Input
|
|
name="name"
|
|
placeholder="Product Name"
|
|
value={productData.name}
|
|
onChange={handleChange}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Description</label>
|
|
<Textarea
|
|
name="description"
|
|
placeholder="Product Description"
|
|
value={productData.description}
|
|
onChange={handleChange}
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Category</label>
|
|
<Select
|
|
value={productData.category}
|
|
onValueChange={(value) =>
|
|
setProductData({ ...productData, category: value })
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select a category" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{categories.map((cat) => (
|
|
<SelectItem key={cat._id} value={cat._id}>
|
|
{cat.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{productData.category && (
|
|
<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">
|
|
<div className="flex-1 space-y-1">
|
|
<label className="text-xs text-muted-foreground">
|
|
Quantity
|
|
</label>
|
|
<Input
|
|
name="minQuantity"
|
|
type="number"
|
|
min="1"
|
|
placeholder="Quantity"
|
|
value={tier.minQuantity}
|
|
onChange={(e) => handleTieredPricingChangeInternal(e, index)}
|
|
/>
|
|
</div>
|
|
<div className="flex-1 space-y-1">
|
|
<label className="text-xs text-muted-foreground">
|
|
Price
|
|
</label>
|
|
<Input
|
|
name="pricePerUnit"
|
|
type="number"
|
|
step="0.01"
|
|
placeholder="Price"
|
|
value={tier.pricePerUnit}
|
|
onChange={(e) => handleTieredPricingChangeInternal(e, index)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="mt-2"
|
|
onClick={() =>
|
|
setProductData({
|
|
...productData,
|
|
tieredPricing: [
|
|
...productData.tieredPricing,
|
|
{ minQuantity: 1, pricePerUnit: 0 }, // Initialize pricePerUnit as a number
|
|
],
|
|
})
|
|
}
|
|
>
|
|
+ Add Tier
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Unit Type</label>
|
|
<Select
|
|
value={productData.unitType}
|
|
onValueChange={(value) =>
|
|
setProductData({ ...productData, unitType: value })
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select unit" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="pcs">Pieces</SelectItem>
|
|
<SelectItem value="gr">Grams</SelectItem>
|
|
<SelectItem value="kg">Kilograms</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Image Upload Section */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Product Image</label>
|
|
<Input
|
|
type="file"
|
|
accept="image/*"
|
|
className="file:text-foreground"
|
|
onChange={handleImageChange}
|
|
/>
|
|
{imagePreview && (
|
|
<div className="mt-2">
|
|
<img
|
|
src={imagePreview}
|
|
alt="Product preview"
|
|
className="rounded-md border w-32 h-32 object-cover"
|
|
/>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Click the upload button above to change image
|
|
</p>
|
|
</div>
|
|
)}
|
|
{!imagePreview &&
|
|
productData.image &&
|
|
typeof productData.image === "string" && (
|
|
<div className="mt-2">
|
|
<img
|
|
src={productData.image}
|
|
alt="Existing product"
|
|
className="rounded-md border w-32 h-32 object-cover"
|
|
/>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Existing product image
|
|
</p>
|
|
</div>
|
|
)}
|
|
<p className="text-xs text-muted-foreground">
|
|
Upload a product image (JPEG, PNG, WEBP)
|
|
</p>
|
|
</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>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|