Refactor pricing tiers form UI and logic

Simplifies the pricing tiers form by removing help text, card layout, and validation logic. Switches to a compact grid-based UI, adds editable total price field, and streamlines event handling for tier changes. Improves user experience and code maintainability.
This commit is contained in:
NotII
2025-10-09 20:54:33 +01:00
parent 051d33df33
commit 32bf9d790f

View File

@@ -2,10 +2,7 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Trash, PlusCircle } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Trash, PlusCircle, Info } from "lucide-react";
import { useState } from "react";
interface PricingTiersProps { interface PricingTiersProps {
pricing: any[]; pricing: any[];
@@ -23,175 +20,112 @@ export const PricingTiers = ({
handleRemoveTier, handleRemoveTier,
handleAddTier, handleAddTier,
}: PricingTiersProps) => { }: PricingTiersProps) => {
const [showHelp, setShowHelp] = useState(false);
const formatNumber = (num: number) => { const formatNumber = (num: number) => {
if (num === 0) return ""; // Only format to 2 decimal places if the number has decimal places
// This prevents cursor jumping when user types whole numbers
return num % 1 === 0 ? num.toString() : num.toFixed(2); return num % 1 === 0 ? num.toString() : num.toFixed(2);
}; };
const formatTotal = (num: number) => {
return num.toFixed(2);
};
const calculateTotal = (quantity: number, pricePerUnit: number) => { const calculateTotal = (quantity: number, pricePerUnit: number) => {
return (quantity * pricePerUnit).toFixed(2); return formatTotal(quantity * pricePerUnit);
}; };
const validateTier = (tier: any, index: number) => { const handleTotalChange = (
const errors = []; e: React.ChangeEvent<HTMLInputElement>,
index: number,
minQuantity: number
) => {
const totalPrice = Number(e.target.value);
const pricePerUnit = minQuantity > 0 ? totalPrice / minQuantity : 0;
// Only validate if both fields have values const syntheticEvent = {
if (tier.minQuantity > 0 && tier.pricePerUnit > 0) { target: {
// Check for duplicate quantities only if both fields are complete name: 'pricePerUnit',
const duplicateIndex = pricing.findIndex((p, i) => value: formatNumber(pricePerUnit)
i !== index && p.minQuantity === tier.minQuantity && p.minQuantity > 0
);
if (duplicateIndex !== -1) {
errors.push("Duplicate quantity found");
} }
} } as React.ChangeEvent<HTMLInputElement>;
// Only show validation errors for completed fields handleTierChange(syntheticEvent, index);
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 ( return (
<div className="space-y-4"> <div>
<div className="flex items-center justify-between"> <h3 className="text-sm font-medium">Tiered Pricing</h3>
<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 ? ( {pricing?.length > 0 ? (
<div className="space-y-3"> <>
{sortedPricing.map((tier, sortedIndex) => { <div className="grid grid-cols-[1fr_1fr_1fr_auto] gap-2 mt-2 mb-1">
const originalIndex = pricing.findIndex(p => p === tier); <div className="text-xs text-muted-foreground">Quantity</div>
const errors = validateTier(tier, originalIndex); <div className="text-xs text-muted-foreground">Price Per Unit</div>
const total = tier.minQuantity && tier.pricePerUnit <div className="text-xs text-muted-foreground">Total Price</div>
? calculateTotal(tier.minQuantity, tier.pricePerUnit) <div className="w-8" />
: "0.00"; </div>
return ( {[...pricing]
<Card key={tier._id || originalIndex} className={`p-4 ${errors.length > 0 ? 'border-red-200 bg-red-50' : 'border-gray-200'}`}> .sort((a, b) => a.minQuantity - b.minQuantity)
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> .map((tier, sortedIndex) => {
<div className="space-y-2"> // Find the original index for proper event handling
<Label htmlFor={`quantity-${originalIndex}`} className="text-xs font-medium"> const originalIndex = pricing.findIndex(p =>
Minimum Quantity * p === tier || (p.minQuantity === tier.minQuantity && p.pricePerUnit === tier.pricePerUnit)
</Label> );
<Input return (
id={`quantity-${originalIndex}`} <div
name="minQuantity" key={tier._id || originalIndex}
type="number" className="grid grid-cols-[1fr_1fr_1fr_auto] gap-2 mt-2"
min="1" >
step="1" <Input
placeholder="e.g. 29" name="minQuantity"
value={tier.minQuantity === 0 ? "" : tier.minQuantity} type="number"
onChange={(e) => handleTierChange(e, originalIndex)} placeholder="Min Quantity"
className={`h-10 ${errors.some(e => e.includes('Quantity') || e.includes('Duplicate')) ? 'border-red-500' : ''}`} value={tier.minQuantity === 0 ? "" : tier.minQuantity}
/> onChange={(e) => handleTierChange(e, originalIndex)}
{errors.some(e => e.includes('Quantity')) && ( className="h-8 text-sm px-2"
<p className="text-xs text-red-600">{errors.find(e => e.includes('Quantity'))}</p> />
)}
</div>
<div className="space-y-2"> <Input
<Label htmlFor={`price-${originalIndex}`} className="text-xs font-medium"> name="pricePerUnit"
Price Per Unit (£) * type="number"
</Label> placeholder="Price per unit"
<Input value={tier.pricePerUnit === 0 ? "" : formatNumber(tier.pricePerUnit)}
id={`price-${originalIndex}`} onChange={(e) => handleTierChange(e, originalIndex)}
name="pricePerUnit" className="h-8 text-sm px-2"
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"> <Input
<Label className="text-xs font-medium text-muted-foreground"> type="number"
Total Price placeholder="Total price"
</Label> value={
<div className="h-10 px-3 py-2 bg-gray-50 border border-gray-200 rounded-md flex items-center"> tier.minQuantity && tier.pricePerUnit
<span className="text-sm font-medium">£{total}</span> ? calculateTotal(tier.minQuantity, tier.pricePerUnit)
</div> : ""
<p className="text-xs text-muted-foreground"> }
{tier.minQuantity} × £{formatNumber(tier.pricePerUnit)} onChange={(e) => handleTotalChange(e, originalIndex, tier.minQuantity)}
</p> className="h-8 text-sm px-2"
</div> />
</div>
{errors.some(e => e.includes('Duplicate')) && tier.minQuantity > 0 && tier.pricePerUnit > 0 && ( <Button
<p className="text-xs text-red-600 mt-2"> variant="ghost"
This quantity is already used in another tier size="icon"
</p> className="text-red-500 hover:bg-red-100"
)} onClick={() => handleRemoveTier(originalIndex)}
>
<div className="flex justify-end mt-3"> <Trash className="h-5 w-5" />
<Button </Button>
variant="ghost" </div>
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"> <p className="text-sm text-gray-500 mt-2">No pricing tiers added.</p>
<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 <Button variant="outline" size="sm" className="mt-2" onClick={handleAddTier}>
variant="outline" <PlusCircle className="w-4 h-4 mr-1" />
onClick={handleAddTier} Add Tier
className="w-full"
>
<PlusCircle className="w-4 h-4 mr-2" />
Add Pricing Tier
</Button> </Button>
</div> </div>
); );