Cleanup
This commit is contained in:
@@ -28,7 +28,7 @@ export default function ProductsPage() {
|
||||
unitType: "pcs",
|
||||
category: "",
|
||||
pricing: [{ minQuantity: 1, pricePerUnit: 0 }],
|
||||
image: null
|
||||
image: null,
|
||||
});
|
||||
|
||||
// Fetch products and categories
|
||||
@@ -75,6 +75,20 @@ export default function ProductsPage() {
|
||||
fetchDataAsync();
|
||||
}, []);
|
||||
|
||||
const handleAddTier = () => {
|
||||
setProductData((prev) => ({
|
||||
...prev,
|
||||
pricing: [...prev.pricing, { minQuantity: 1, pricePerUnit: 0 }],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRemoveTier = (index: number) => {
|
||||
setProductData((prev) => ({
|
||||
...prev,
|
||||
pricing: prev.pricing.filter((_, i) => i !== index),
|
||||
}));
|
||||
};
|
||||
|
||||
// Handle input changes
|
||||
const handleChange = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
@@ -159,9 +173,9 @@ export default function ProductsPage() {
|
||||
setProductData({
|
||||
...product,
|
||||
pricing: product.pricing
|
||||
? product.pricing.map(tier => ({
|
||||
? product.pricing.map((tier) => ({
|
||||
minQuantity: tier.minQuantity,
|
||||
pricePerUnit: tier.pricePerUnit
|
||||
pricePerUnit: tier.pricePerUnit,
|
||||
}))
|
||||
: [{ minQuantity: 1, pricePerUnit: 0 }], // Fallback if undefined
|
||||
});
|
||||
@@ -219,6 +233,8 @@ export default function ProductsPage() {
|
||||
editing={editing}
|
||||
handleChange={handleChange}
|
||||
handleTieredPricingChange={handleTieredPricingChange}
|
||||
handleAddTier={handleAddTier} // ✅ Ensure this is passed
|
||||
handleRemoveTier={handleRemoveTier} // ✅ Ensure this is passed
|
||||
setProductData={setProductData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
40
components/forms/image-upload.tsx
Normal file
40
components/forms/image-upload.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { ChangeEvent } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface ImageUploadProps {
|
||||
imagePreview: string | null;
|
||||
handleImageChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
imageDimensions: { width: number; height: number };
|
||||
}
|
||||
|
||||
export const ImageUpload = ({
|
||||
imagePreview,
|
||||
handleImageChange,
|
||||
imageDimensions,
|
||||
}: ImageUploadProps) => (
|
||||
<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"
|
||||
style={{ width: imageDimensions.width, height: imageDimensions.height }}
|
||||
>
|
||||
{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>
|
||||
);
|
||||
67
components/forms/pricing-tiers.tsx
Normal file
67
components/forms/pricing-tiers.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Trash, PlusCircle } from "lucide-react";
|
||||
|
||||
interface PricingTiersProps {
|
||||
pricing: any[];
|
||||
handleTierChange: (e: React.ChangeEvent<HTMLInputElement>, index: number) => void;
|
||||
handleRemoveTier: (index: number) => void;
|
||||
handleAddTier: () => void;
|
||||
}
|
||||
|
||||
export const PricingTiers = ({
|
||||
pricing,
|
||||
handleTierChange,
|
||||
handleRemoveTier,
|
||||
handleAddTier,
|
||||
}: PricingTiersProps) => (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Tiered Pricing</h3>
|
||||
|
||||
{pricing?.length > 0 ? (
|
||||
pricing.map((tier, index) => (
|
||||
<div key={tier._id || index} className="flex items-center gap-2 mt-2">
|
||||
<Input
|
||||
name="minQuantity"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="Min Quantity"
|
||||
value={tier.minQuantity}
|
||||
onChange={(e) => handleTierChange(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) => handleTierChange(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={() => handleRemoveTier(index)}
|
||||
>
|
||||
<Trash className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 mt-2">No pricing tiers added.</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={handleAddTier}
|
||||
>
|
||||
<PlusCircle className="w-4 h-4 mr-1" />
|
||||
Add Tier
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -1,58 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { ChangeEvent, useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { 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 { Trash, PlusCircle } from "lucide-react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ImageUpload } from "@/components/forms/image-upload";
|
||||
import { PricingTiers } from "@/components/forms/pricing-tiers";
|
||||
import { ProductModalProps, ProductData } from "@/lib/types";
|
||||
import { toast } from "sonner";
|
||||
import { Product } from "@/models/products";
|
||||
|
||||
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 | undefined;
|
||||
}
|
||||
|
||||
interface ProductModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (productData: ProductData) => void;
|
||||
productData: ProductData;
|
||||
categories: any[];
|
||||
editing: boolean;
|
||||
handleChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||
handleTieredPricingChange: (e: ChangeEvent<HTMLInputElement>, index: number) => void; // ✅ Added this
|
||||
setProductData: React.Dispatch<React.SetStateAction<ProductData>>;
|
||||
}
|
||||
|
||||
export const ProductModal = ({
|
||||
open,
|
||||
@@ -65,11 +22,7 @@ export const ProductModal = ({
|
||||
setProductData,
|
||||
}: ProductModalProps) => {
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||
const [newCategory, setNewCategory] = useState("");
|
||||
const [imageDimensions, setImageDimensions] = useState({
|
||||
width: 300,
|
||||
height: 200,
|
||||
});
|
||||
const [imageDimensions, setImageDimensions] = useState({ width: 300, height: 200 });
|
||||
|
||||
useEffect(() => {
|
||||
if (productData.image && typeof productData.image === "string") {
|
||||
@@ -77,71 +30,60 @@ export const ProductModal = ({
|
||||
}
|
||||
}, [productData.image]);
|
||||
|
||||
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const handleImageChange = (e: React.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(objectUrl);
|
||||
setImageDimensions({ width, height });
|
||||
};
|
||||
|
||||
image.src = objectUrl;
|
||||
} else {
|
||||
if (!file) {
|
||||
setProductData({ ...productData, image: null });
|
||||
setImagePreview(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const image = new Image();
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
|
||||
image.onload = () => {
|
||||
const aspectRatio = image.naturalWidth / image.naturalHeight;
|
||||
const width = aspectRatio > 1 ? 300 : 200 * aspectRatio;
|
||||
const height = aspectRatio > 1 ? 300 / aspectRatio : 200;
|
||||
|
||||
setProductData({ ...productData, image: file });
|
||||
setImagePreview(objectUrl);
|
||||
setImageDimensions({ width, height });
|
||||
};
|
||||
|
||||
image.src = objectUrl;
|
||||
};
|
||||
|
||||
const handleTierChange = (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
|
||||
const { name, valueAsNumber } = e.target;
|
||||
|
||||
if (!["minQuantity", "pricePerUnit"].includes(name)) return; // ✅ Ensure valid keys
|
||||
|
||||
const updatedPricing = [...productData.pricing];
|
||||
(updatedPricing[index] as any)[name] = isNaN(valueAsNumber) ? 0 : valueAsNumber;
|
||||
|
||||
setProductData({ ...productData, pricing: updatedPricing });
|
||||
};
|
||||
|
||||
|
||||
const handleAddTier = () => {
|
||||
setProductData(prev => ({
|
||||
...prev,
|
||||
pricing: [...prev.pricing, { minQuantity: 1, pricePerUnit: 0 }]
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRemoveTier = (index: number) => {
|
||||
const updatedPricing = productData.pricing.filter((_, i) => i !== index);
|
||||
setProductData({ ...productData, pricing: updatedPricing });
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(productData);
|
||||
toast.success(
|
||||
editing ? "Product updated successfully!" : "Product added successfully!"
|
||||
);
|
||||
toast.success(editing ? "Product updated!" : "Product added!");
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleTieredPricingChange = (
|
||||
e: ChangeEvent<HTMLInputElement>,
|
||||
index: number
|
||||
) => {
|
||||
const { name, valueAsNumber } = e.target;
|
||||
|
||||
if (!productData.pricing) return;
|
||||
const updatedPricing = [...productData.pricing];
|
||||
|
||||
updatedPricing[index] = {
|
||||
...updatedPricing[index],
|
||||
[name]: isNaN(valueAsNumber) ? 0 : valueAsNumber,
|
||||
};
|
||||
|
||||
setProductData((prev) => ({
|
||||
...prev,
|
||||
tieredPricing: updatedPricing,
|
||||
image: prev.image ?? null,
|
||||
}));
|
||||
|
||||
const convertToProductData = (product: Product): ProductData => ({
|
||||
...product,
|
||||
image: product.image ?? null, // ✅ Ensures the type is correct
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-6xl">
|
||||
@@ -152,216 +94,155 @@ export const ProductModal = ({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-8 py-4">
|
||||
{/* Left Column */}
|
||||
<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}
|
||||
className="text-sm h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="text-sm font-medium">Description</label>
|
||||
<Textarea
|
||||
name="description"
|
||||
placeholder="Product Description"
|
||||
value={productData.description}
|
||||
onChange={handleChange}
|
||||
rows={4}
|
||||
className="text-sm h-24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category and Unit Type */}
|
||||
<div>
|
||||
<label className="text-sm font-medium">Category</label>
|
||||
<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}
|
||||
</SelectItem>
|
||||
))}
|
||||
</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>
|
||||
<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>
|
||||
<ProductBasicInfo
|
||||
productData={productData}
|
||||
handleChange={handleChange}
|
||||
categories={categories}
|
||||
setProductData={setProductData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Column 2: Image and Tiered Pricing */}
|
||||
{/* Right Column */}
|
||||
<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>
|
||||
<ImageUpload
|
||||
imagePreview={imagePreview}
|
||||
handleImageChange={handleImageChange}
|
||||
imageDimensions={imageDimensions}
|
||||
/>
|
||||
<PricingTiers
|
||||
pricing={productData.pricing}
|
||||
handleTierChange={handleTierChange}
|
||||
handleRemoveTier={handleRemoveTier}
|
||||
handleAddTier={handleAddTier}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-end gap-2">
|
||||
<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"}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={handleSave}>
|
||||
{editing ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const ProductBasicInfo = ({
|
||||
productData,
|
||||
handleChange,
|
||||
categories,
|
||||
setProductData,
|
||||
}: {
|
||||
productData: ProductData;
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||
categories: { _id: string; name: string }[];
|
||||
setProductData: React.Dispatch<React.SetStateAction<ProductData>>;
|
||||
}) => (
|
||||
<>
|
||||
<div>
|
||||
<label className="text-sm font-medium">Product Name</label>
|
||||
<Input
|
||||
name="name"
|
||||
placeholder="Product Name"
|
||||
value={productData.name}
|
||||
onChange={handleChange}
|
||||
className="text-sm h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium">Description</label>
|
||||
<Textarea
|
||||
name="description"
|
||||
placeholder="Product Description"
|
||||
value={productData.description}
|
||||
onChange={handleChange}
|
||||
rows={4}
|
||||
className="text-sm h-24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CategorySelect
|
||||
categories={categories}
|
||||
value={productData.category}
|
||||
setProductData={setProductData}
|
||||
/>
|
||||
|
||||
<UnitTypeSelect
|
||||
value={productData.unitType}
|
||||
setProductData={setProductData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const CategorySelect = ({
|
||||
categories,
|
||||
value,
|
||||
setProductData,
|
||||
}: {
|
||||
categories: { _id: string; name: string }[];
|
||||
value: string;
|
||||
setProductData: React.Dispatch<React.SetStateAction<ProductData>>;
|
||||
}) => (
|
||||
<div>
|
||||
<label className="text-sm font-medium">Category</label>
|
||||
<Select
|
||||
value={value || "placeholder"}
|
||||
onValueChange={(val) =>
|
||||
setProductData((prev) => ({
|
||||
...prev,
|
||||
category: val === "placeholder" ? "" : val,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select category..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="placeholder">Select category...</SelectItem>
|
||||
{categories.map((cat) => (
|
||||
<SelectItem key={cat._id} value={cat._id}>
|
||||
{cat.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
|
||||
const UnitTypeSelect = ({
|
||||
value,
|
||||
setProductData,
|
||||
}: {
|
||||
value: string;
|
||||
setProductData: React.Dispatch<React.SetStateAction<ProductData>>;
|
||||
}) => (
|
||||
<div>
|
||||
<label className="text-sm font-medium">Unit Type</label>
|
||||
<Select
|
||||
value={value || "placeholder"}
|
||||
onValueChange={(val) =>
|
||||
setProductData((prev: ProductData) => ({
|
||||
...prev,
|
||||
unitType: val === "placeholder" ? "" : val,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select unit type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="placeholder" disabled>
|
||||
Select unit type
|
||||
</SelectItem>
|
||||
<SelectItem value="pcs">Pieces (pcs)</SelectItem>
|
||||
<SelectItem value="gr">Grams (gr)</SelectItem>
|
||||
<SelectItem value="kg">Kilograms (kg)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
62
lib/types.ts
62
lib/types.ts
@@ -1,3 +1,19 @@
|
||||
import { ChangeEvent, Dispatch, SetStateAction } from "react";
|
||||
|
||||
export interface ProductModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (productData: ProductData) => void;
|
||||
productData: ProductData;
|
||||
categories: Category[];
|
||||
editing: boolean;
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||
handleTieredPricingChange: (e: React.ChangeEvent<HTMLInputElement>, index: number) => void;
|
||||
handleAddTier: () => void; // ✅ ADDED
|
||||
handleRemoveTier: (index: number) => void; // ✅ ADDED
|
||||
setProductData: React.Dispatch<React.SetStateAction<ProductData>>;
|
||||
}
|
||||
|
||||
// lib/types.ts
|
||||
export interface ShippingMethod {
|
||||
_id?: string; // Optional before saving, required after fetching
|
||||
@@ -11,19 +27,43 @@ export interface ShippingData {
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
_id?: string;
|
||||
name: string;
|
||||
price: number;
|
||||
description?: string;
|
||||
sku?: string;
|
||||
stock: number;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export type ApiResponse<T> = {
|
||||
data?: T;
|
||||
error?: string;
|
||||
total?: number;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export interface Product {
|
||||
_id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
stock?: number;
|
||||
unitType: string;
|
||||
category: string;
|
||||
pricing: PricingTier[];
|
||||
image?: string | File | null;
|
||||
}
|
||||
|
||||
export interface ProductData {
|
||||
_id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
stock?: number;
|
||||
unitType: string;
|
||||
category: string;
|
||||
pricing: PricingTier[];
|
||||
image?: string | File | null;
|
||||
}
|
||||
|
||||
export interface PricingTier {
|
||||
minQuantity: number;
|
||||
pricePerUnit: number;
|
||||
_id?: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
_id: string;
|
||||
name: string;
|
||||
}
|
||||
Reference in New Issue
Block a user