Files
ember-market-frontend/components/forms/pricing-tiers.tsx
NotII a912967fd4 Revamp pricing tiers form UI and validation
Improves the pricing tiers form with enhanced UI using Card and Label components, adds help text, and introduces validation for quantity, price, and duplicate tiers. The form now displays errors inline and sorts tiers by quantity for better usability.
2025-10-09 20:43:44 +01:00

195 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 = [];
if (tier.minQuantity <= 0) {
errors.push("Quantity must be greater than 0");
}
if (tier.pricePerUnit <= 0) {
errors.push("Price must be greater than 0");
}
// Check for duplicate quantities
const duplicateIndex = pricing.findIndex((p, i) =>
i !== index && p.minQuantity === tier.minQuantity
);
if (duplicateIndex !== -1) {
errors.push("Duplicate quantity found");
}
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. 10"
value={tier.minQuantity === 0 ? "" : tier.minQuantity}
onChange={(e) => handleTierChange(e, originalIndex)}
className={`h-10 ${errors.some(e => e.includes('Quantity')) ? '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')) && (
<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>
);
};