:D
This commit is contained in:
@@ -105,7 +105,6 @@ export default function ProductsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Save product data after modal form submission
|
|
||||||
const handleSaveProduct = async (data: Product, file?: File | null) => {
|
const handleSaveProduct = async (data: Product, file?: File | null) => {
|
||||||
console.log("handleSaveProduct:", data, file);
|
console.log("handleSaveProduct:", data, file);
|
||||||
|
|
||||||
|
|||||||
@@ -19,52 +19,106 @@ export const PricingTiers = ({
|
|||||||
handleTierChange,
|
handleTierChange,
|
||||||
handleRemoveTier,
|
handleRemoveTier,
|
||||||
handleAddTier,
|
handleAddTier,
|
||||||
}: PricingTiersProps) => (
|
}: PricingTiersProps) => {
|
||||||
<div>
|
const formatNumber = (num: number) => {
|
||||||
<h3 className="text-sm font-medium">Tiered Pricing</h3>
|
// For price per unit, show up to 6 decimal places if needed
|
||||||
|
return Number(num.toFixed(6)).toString();
|
||||||
|
};
|
||||||
|
|
||||||
{pricing?.length > 0 ? (
|
const formatTotal = (num: number) => {
|
||||||
pricing.map((tier, index) => (
|
// For total price, always show 2 decimal places
|
||||||
<div key={tier._id || index} className="flex items-center gap-2 mt-2">
|
return num.toFixed(2);
|
||||||
<Input
|
};
|
||||||
name="minQuantity"
|
|
||||||
type="number"
|
|
||||||
placeholder="Min Quantity"
|
|
||||||
value={tier.minQuantity === 0 ? "" : tier.minQuantity} // ✅ Show empty string when 0
|
|
||||||
onChange={(e) => handleTierChange(e, index)}
|
|
||||||
className="h-8 text-sm px-2 flex-1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
const calculateTotal = (quantity: number, pricePerUnit: number) => {
|
||||||
name="pricePerUnit"
|
return formatTotal(quantity * pricePerUnit);
|
||||||
type="number"
|
};
|
||||||
placeholder="Price per unit"
|
|
||||||
value={tier.pricePerUnit === 0 ? "" : tier.pricePerUnit} // ✅ Show empty string when 0
|
|
||||||
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
|
const handleTotalChange = (
|
||||||
variant="outline"
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
size="sm"
|
index: number,
|
||||||
className="mt-2"
|
minQuantity: number
|
||||||
onClick={handleAddTier}
|
) => {
|
||||||
>
|
const totalPrice = Number(e.target.value);
|
||||||
<PlusCircle className="w-4 h-4 mr-1" />
|
const pricePerUnit = minQuantity > 0 ? totalPrice / minQuantity : 0;
|
||||||
Add Tier
|
|
||||||
</Button>
|
const syntheticEvent = {
|
||||||
</div>
|
target: {
|
||||||
);
|
name: 'pricePerUnit',
|
||||||
|
value: formatNumber(pricePerUnit)
|
||||||
|
}
|
||||||
|
} as React.ChangeEvent<HTMLInputElement>;
|
||||||
|
|
||||||
|
handleTierChange(syntheticEvent, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium">Tiered Pricing</h3>
|
||||||
|
|
||||||
|
{pricing?.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-[1fr_1fr_1fr_auto] gap-2 mt-2 mb-1">
|
||||||
|
<div className="text-xs text-muted-foreground">Quantity</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Price Per Unit</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Total Price</div>
|
||||||
|
<div className="w-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pricing.map((tier, index) => (
|
||||||
|
<div
|
||||||
|
key={tier._id || index}
|
||||||
|
className="grid grid-cols-[1fr_1fr_1fr_auto] gap-2 mt-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="minQuantity"
|
||||||
|
type="number"
|
||||||
|
placeholder="Min Quantity"
|
||||||
|
value={tier.minQuantity === 0 ? "" : tier.minQuantity}
|
||||||
|
onChange={(e) => handleTierChange(e, index)}
|
||||||
|
className="h-8 text-sm px-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
name="pricePerUnit"
|
||||||
|
type="number"
|
||||||
|
placeholder="Price per unit"
|
||||||
|
value={tier.pricePerUnit === 0 ? "" : formatNumber(tier.pricePerUnit)}
|
||||||
|
onChange={(e) => handleTierChange(e, index)}
|
||||||
|
className="h-8 text-sm px-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Total price"
|
||||||
|
value={
|
||||||
|
tier.minQuantity && tier.pricePerUnit
|
||||||
|
? calculateTotal(tier.minQuantity, tier.pricePerUnit)
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onChange={(e) => handleTotalChange(e, index, tier.minQuantity)}
|
||||||
|
className="h-8 text-sm px-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -87,10 +87,10 @@ export const ProductModal: React.FC<ProductModalProps> = ({
|
|||||||
const handleAddTier = () => {
|
const handleAddTier = () => {
|
||||||
setProductData((prev) => ({
|
setProductData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
pricing: [...prev.pricing, { minQuantity: 0, pricePerUnit: 0 }],
|
pricing: [...prev.pricing, { minQuantity: 0, pricePerUnit: 0 }],
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!productData.category) {
|
if (!productData.category) {
|
||||||
toast.error("Please select or add a category");
|
toast.error("Please select or add a category");
|
||||||
@@ -114,7 +114,7 @@ export const ProductModal: React.FC<ProductModalProps> = ({
|
|||||||
...prev,
|
...prev,
|
||||||
pricing: prev.pricing.map((tier, i) =>
|
pricing: prev.pricing.map((tier, i) =>
|
||||||
i === index
|
i === index
|
||||||
? { ...tier, [name]: value === "" ? 0 : Number(value) }
|
? { ...tier, [name]: value === "" ? 0 : Number(value) }
|
||||||
: tier
|
: tier
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
@@ -122,7 +122,7 @@ export const ProductModal: React.FC<ProductModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
<DialogContent className="max-w-6xl">
|
<DialogContent className="max-w-[95vw] lg:max-w-6xl w-full overflow-y-auto max-h-[90vh] z-[80]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-base">
|
<DialogTitle className="text-base">
|
||||||
{editing ? "Edit Product" : "Add Product"}
|
{editing ? "Edit Product" : "Add Product"}
|
||||||
@@ -221,9 +221,9 @@ const CategorySelect: React.FC<CategorySelectProps> = ({
|
|||||||
|
|
||||||
// Get root categories (those without parentId)
|
// Get root categories (those without parentId)
|
||||||
const rootCategories = categories.filter(cat => !cat.parentId);
|
const rootCategories = categories.filter(cat => !cat.parentId);
|
||||||
|
|
||||||
// Get subcategories for a given parent
|
// Get subcategories for a given parent
|
||||||
const getSubcategories = (parentId: string) =>
|
const getSubcategories = (parentId: string) =>
|
||||||
categories.filter(cat => cat.parentId === parentId);
|
categories.filter(cat => cat.parentId === parentId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ const ProductTable = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
getCategoryNameById
|
getCategoryNameById
|
||||||
}: ProductTableProps) => {
|
}: ProductTableProps) => {
|
||||||
|
|
||||||
|
const sortedProducts = [...products].sort((a, b) => {
|
||||||
|
const categoryNameA = getCategoryNameById(a.category);
|
||||||
|
const categoryNameB = getCategoryNameById(b.category);
|
||||||
|
return categoryNameA.localeCompare(categoryNameB);
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden">
|
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden">
|
||||||
<Table className="relative">
|
<Table className="relative">
|
||||||
@@ -39,8 +45,8 @@ const ProductTable = ({
|
|||||||
<TableCell>Loading...</TableCell>
|
<TableCell>Loading...</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
) : products.length > 0 ? (
|
) : sortedProducts.length > 0 ? (
|
||||||
products.map((product) => (
|
sortedProducts.map((product) => (
|
||||||
<TableRow key={product._id} className="transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70">
|
<TableRow key={product._id} className="transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70">
|
||||||
<TableCell className="font-medium">{product.name}</TableCell>
|
<TableCell className="font-medium">{product.name}</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
|||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"fixed inset-0 z-[75] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"fixed left-[50%] top-[50%] z-[76] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in New Issue
Block a user