Validation now only checks for duplicate quantities when both minQuantity and pricePerUnit are set and positive. Error messages and input styling are updated to reflect more accurate validation states, and the placeholder for minQuantity is changed to 'e.g. 29'.
199 lines
7.3 KiB
TypeScript
199 lines
7.3 KiB
TypeScript
"use client";
|
||
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Card, CardContent } from "@/components/ui/card";
|
||
import { Trash, PlusCircle, Info } from "lucide-react";
|
||
import { useState } from "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) => {
|
||
const [showHelp, setShowHelp] = useState(false);
|
||
|
||
const formatNumber = (num: number) => {
|
||
if (num === 0) return "";
|
||
return num % 1 === 0 ? num.toString() : num.toFixed(2);
|
||
};
|
||
|
||
const calculateTotal = (quantity: number, pricePerUnit: number) => {
|
||
return (quantity * pricePerUnit).toFixed(2);
|
||
};
|
||
|
||
const validateTier = (tier: any, index: number) => {
|
||
const errors = [];
|
||
|
||
// Only validate if both fields have values
|
||
if (tier.minQuantity > 0 && tier.pricePerUnit > 0) {
|
||
// Check for duplicate quantities only if both fields are complete
|
||
const duplicateIndex = pricing.findIndex((p, i) =>
|
||
i !== index && p.minQuantity === tier.minQuantity && p.minQuantity > 0
|
||
);
|
||
|
||
if (duplicateIndex !== -1) {
|
||
errors.push("Duplicate quantity found");
|
||
}
|
||
}
|
||
|
||
// Only show validation errors for completed fields
|
||
if (tier.minQuantity !== 0 && tier.minQuantity <= 0) {
|
||
errors.push("Quantity must be greater than 0");
|
||
}
|
||
|
||
if (tier.pricePerUnit !== 0 && tier.pricePerUnit <= 0) {
|
||
errors.push("Price must be greater than 0");
|
||
}
|
||
|
||
return errors;
|
||
};
|
||
|
||
const sortedPricing = [...pricing].sort((a, b) => a.minQuantity - b.minQuantity);
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="text-sm font-medium">Pricing Tiers</h3>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setShowHelp(!showHelp)}
|
||
className="text-xs text-muted-foreground"
|
||
>
|
||
<Info className="w-3 h-3 mr-1" />
|
||
{showHelp ? "Hide" : "Help"}
|
||
</Button>
|
||
</div>
|
||
|
||
{showHelp && (
|
||
<Card className="bg-blue-50 border-blue-200">
|
||
<CardContent className="p-3">
|
||
<p className="text-xs text-blue-800">
|
||
<strong>How it works:</strong> Set different prices based on quantity.
|
||
For example: 1-10 units at £5 each, 11-50 units at £4 each, 51+ units at £3 each.
|
||
Quantities should be in ascending order.
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{pricing?.length > 0 ? (
|
||
<div className="space-y-3">
|
||
{sortedPricing.map((tier, sortedIndex) => {
|
||
const originalIndex = pricing.findIndex(p => p === tier);
|
||
const errors = validateTier(tier, originalIndex);
|
||
const total = tier.minQuantity && tier.pricePerUnit
|
||
? calculateTotal(tier.minQuantity, tier.pricePerUnit)
|
||
: "0.00";
|
||
|
||
return (
|
||
<Card key={tier._id || originalIndex} className={`p-4 ${errors.length > 0 ? 'border-red-200 bg-red-50' : 'border-gray-200'}`}>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor={`quantity-${originalIndex}`} className="text-xs font-medium">
|
||
Minimum Quantity *
|
||
</Label>
|
||
<Input
|
||
id={`quantity-${originalIndex}`}
|
||
name="minQuantity"
|
||
type="number"
|
||
min="1"
|
||
step="1"
|
||
placeholder="e.g. 29"
|
||
value={tier.minQuantity === 0 ? "" : tier.minQuantity}
|
||
onChange={(e) => handleTierChange(e, originalIndex)}
|
||
className={`h-10 ${errors.some(e => e.includes('Quantity') || e.includes('Duplicate')) ? 'border-red-500' : ''}`}
|
||
/>
|
||
{errors.some(e => e.includes('Quantity')) && (
|
||
<p className="text-xs text-red-600">{errors.find(e => e.includes('Quantity'))}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor={`price-${originalIndex}`} className="text-xs font-medium">
|
||
Price Per Unit (£) *
|
||
</Label>
|
||
<Input
|
||
id={`price-${originalIndex}`}
|
||
name="pricePerUnit"
|
||
type="number"
|
||
min="0"
|
||
step="0.01"
|
||
placeholder="e.g. 5.00"
|
||
value={tier.pricePerUnit === 0 ? "" : formatNumber(tier.pricePerUnit)}
|
||
onChange={(e) => handleTierChange(e, originalIndex)}
|
||
className={`h-10 ${errors.some(e => e.includes('Price')) ? 'border-red-500' : ''}`}
|
||
/>
|
||
{errors.some(e => e.includes('Price')) && (
|
||
<p className="text-xs text-red-600">{errors.find(e => e.includes('Price'))}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-xs font-medium text-muted-foreground">
|
||
Total Price
|
||
</Label>
|
||
<div className="h-10 px-3 py-2 bg-gray-50 border border-gray-200 rounded-md flex items-center">
|
||
<span className="text-sm font-medium">£{total}</span>
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">
|
||
{tier.minQuantity} × £{formatNumber(tier.pricePerUnit)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{errors.some(e => e.includes('Duplicate')) && tier.minQuantity > 0 && tier.pricePerUnit > 0 && (
|
||
<p className="text-xs text-red-600 mt-2">
|
||
⚠️ This quantity is already used in another tier
|
||
</p>
|
||
)}
|
||
|
||
<div className="flex justify-end mt-3">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleRemoveTier(originalIndex)}
|
||
className="text-red-600 hover:text-red-700 hover:bg-red-100"
|
||
>
|
||
<Trash className="w-4 h-4 mr-1" />
|
||
Remove
|
||
</Button>
|
||
</div>
|
||
</Card>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
<Card className="border-dashed border-2 border-gray-300">
|
||
<CardContent className="p-6 text-center">
|
||
<p className="text-sm text-gray-500 mb-2">No pricing tiers added yet</p>
|
||
<p className="text-xs text-gray-400">Add your first tier to get started</p>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleAddTier}
|
||
className="w-full"
|
||
>
|
||
<PlusCircle className="w-4 h-4 mr-2" />
|
||
Add Pricing Tier
|
||
</Button>
|
||
</div>
|
||
);
|
||
};
|