From a912967fd40cb3bc8764afae822558a05bf8c5fc Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:43:44 +0100 Subject: [PATCH] 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. --- components/forms/pricing-tiers.tsx | 246 ++++++++++++++++++----------- 1 file changed, 154 insertions(+), 92 deletions(-) diff --git a/components/forms/pricing-tiers.tsx b/components/forms/pricing-tiers.tsx index 578a3ef..2f246d4 100644 --- a/components/forms/pricing-tiers.tsx +++ b/components/forms/pricing-tiers.tsx @@ -2,7 +2,10 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Trash, PlusCircle } from "lucide-react"; +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[]; @@ -20,112 +23,171 @@ export const PricingTiers = ({ handleRemoveTier, handleAddTier, }: PricingTiersProps) => { + const [showHelp, setShowHelp] = useState(false); + const formatNumber = (num: number) => { - // Only format to 2 decimal places if the number has decimal places - // This prevents cursor jumping when user types whole numbers + if (num === 0) return ""; return num % 1 === 0 ? num.toString() : num.toFixed(2); }; - const formatTotal = (num: number) => { - return num.toFixed(2); - }; - const calculateTotal = (quantity: number, pricePerUnit: number) => { - return formatTotal(quantity * pricePerUnit); + return (quantity * pricePerUnit).toFixed(2); }; - const handleTotalChange = ( - e: React.ChangeEvent, - index: number, - minQuantity: number - ) => { - const totalPrice = Number(e.target.value); - const pricePerUnit = minQuantity > 0 ? totalPrice / minQuantity : 0; + const validateTier = (tier: any, index: number) => { + const errors = []; - const syntheticEvent = { - target: { - name: 'pricePerUnit', - value: formatNumber(pricePerUnit) - } - } as React.ChangeEvent; + if (tier.minQuantity <= 0) { + errors.push("Quantity must be greater than 0"); + } - handleTierChange(syntheticEvent, index); + 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 ( -
-

Tiered Pricing

+
+
+

Pricing Tiers

+ +
- {pricing?.length > 0 ? ( - <> -
-
Quantity
-
Price Per Unit
-
Total Price
-
-
- - {[...pricing] - .sort((a, b) => a.minQuantity - b.minQuantity) - .map((tier, sortedIndex) => { - // Find the original index for proper event handling - const originalIndex = pricing.findIndex(p => - p === tier || (p.minQuantity === tier.minQuantity && p.pricePerUnit === tier.pricePerUnit) - ); - return ( -
- handleTierChange(e, originalIndex)} - className="h-8 text-sm px-2" - /> - - handleTierChange(e, originalIndex)} - className="h-8 text-sm px-2" - /> - - handleTotalChange(e, originalIndex, tier.minQuantity)} - className="h-8 text-sm px-2" - /> - - -
- ); - })} - - ) : ( -

No pricing tiers added.

+ {showHelp && ( + + +

+ How it works: 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. +

+
+
)} - +
+ + ); + })} +
+ ) : ( + + +

No pricing tiers added yet

+

Add your first tier to get started

+
+
+ )} + +
);