Improve pricing tier input handling and precision
Refactored PricingTiers component to better handle empty, null, and undefined values, prevent formatting issues, and add error handling for tier changes. Updated ProductModal to use parseFloat for price values to improve precision. Minor robustness improvements to event handling and sorting.
This commit is contained in:
@@ -15,15 +15,15 @@ interface PricingTiersProps {
|
||||
}
|
||||
|
||||
export const PricingTiers = ({
|
||||
pricing,
|
||||
pricing = [],
|
||||
handleTierChange,
|
||||
handleRemoveTier,
|
||||
handleAddTier,
|
||||
}: PricingTiersProps) => {
|
||||
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
|
||||
return num % 1 === 0 ? num.toString() : num.toFixed(2);
|
||||
if (isNaN(num) || num === null || num === undefined) return "";
|
||||
// Return the number as-is without any formatting to prevent precision issues
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
const formatTotal = (num: number) => {
|
||||
@@ -39,17 +39,24 @@ export const PricingTiers = ({
|
||||
index: number,
|
||||
minQuantity: number
|
||||
) => {
|
||||
if (!handleTierChange || !e || !e.target) return;
|
||||
|
||||
const totalPrice = Number(e.target.value);
|
||||
const pricePerUnit = minQuantity > 0 ? totalPrice / minQuantity : 0;
|
||||
|
||||
// Create a simple synthetic event with the raw number
|
||||
const syntheticEvent = {
|
||||
target: {
|
||||
name: 'pricePerUnit',
|
||||
value: formatNumber(pricePerUnit)
|
||||
value: pricePerUnit.toString()
|
||||
}
|
||||
} as React.ChangeEvent<HTMLInputElement>;
|
||||
|
||||
try {
|
||||
handleTierChange(syntheticEvent, index);
|
||||
} catch (error) {
|
||||
console.error('Error in handleTotalChange:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -66,11 +73,13 @@ export const PricingTiers = ({
|
||||
</div>
|
||||
|
||||
{[...pricing]
|
||||
.sort((a, b) => a.minQuantity - b.minQuantity)
|
||||
.sort((a, b) => (a?.minQuantity || 0) - (b?.minQuantity || 0))
|
||||
.map((tier, sortedIndex) => {
|
||||
if (!tier) return null;
|
||||
|
||||
// Find the original index for proper event handling
|
||||
const originalIndex = pricing.findIndex(p =>
|
||||
p === tier || (p.minQuantity === tier.minQuantity && p.pricePerUnit === tier.pricePerUnit)
|
||||
p === tier || (p?.minQuantity === tier?.minQuantity && p?.pricePerUnit === tier?.pricePerUnit)
|
||||
);
|
||||
return (
|
||||
<div
|
||||
@@ -81,8 +90,8 @@ export const PricingTiers = ({
|
||||
name="minQuantity"
|
||||
type="number"
|
||||
placeholder="Min Quantity"
|
||||
value={tier.minQuantity === 0 ? "" : tier.minQuantity}
|
||||
onChange={(e) => handleTierChange(e, originalIndex)}
|
||||
value={tier?.minQuantity === 0 ? "" : (tier?.minQuantity || "")}
|
||||
onChange={(e) => handleTierChange?.(e, originalIndex)}
|
||||
className="h-8 text-sm px-2"
|
||||
/>
|
||||
|
||||
@@ -90,8 +99,8 @@ export const PricingTiers = ({
|
||||
name="pricePerUnit"
|
||||
type="number"
|
||||
placeholder="Price per unit"
|
||||
value={tier.pricePerUnit === 0 ? "" : formatNumber(tier.pricePerUnit)}
|
||||
onChange={(e) => handleTierChange(e, originalIndex)}
|
||||
value={tier?.pricePerUnit === 0 ? "" : formatNumber(tier?.pricePerUnit || 0)}
|
||||
onChange={(e) => handleTierChange?.(e, originalIndex)}
|
||||
className="h-8 text-sm px-2"
|
||||
/>
|
||||
|
||||
@@ -99,11 +108,11 @@ export const PricingTiers = ({
|
||||
type="number"
|
||||
placeholder="Total price"
|
||||
value={
|
||||
tier.minQuantity && tier.pricePerUnit
|
||||
tier?.minQuantity && tier?.pricePerUnit
|
||||
? calculateTotal(tier.minQuantity, tier.pricePerUnit)
|
||||
: ""
|
||||
}
|
||||
onChange={(e) => handleTotalChange(e, originalIndex, tier.minQuantity)}
|
||||
onChange={(e) => handleTotalChange(e, originalIndex, tier?.minQuantity || 0)}
|
||||
className="h-8 text-sm px-2"
|
||||
/>
|
||||
|
||||
@@ -111,7 +120,7 @@ export const PricingTiers = ({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-red-500 hover:bg-red-100"
|
||||
onClick={() => handleRemoveTier(originalIndex)}
|
||||
onClick={() => handleRemoveTier?.(originalIndex)}
|
||||
>
|
||||
<Trash className="h-5 w-5" />
|
||||
</Button>
|
||||
@@ -123,7 +132,7 @@ export const PricingTiers = ({
|
||||
<p className="text-sm text-gray-500 mt-2">No pricing tiers added.</p>
|
||||
)}
|
||||
|
||||
<Button variant="outline" size="sm" className="mt-2" onClick={handleAddTier}>
|
||||
<Button variant="outline" size="sm" className="mt-2" onClick={() => handleAddTier?.()}>
|
||||
<PlusCircle className="w-4 h-4 mr-1" />
|
||||
Add Tier
|
||||
</Button>
|
||||
|
||||
@@ -142,7 +142,7 @@ export const ProductModal: React.FC<ProductModalProps> = ({
|
||||
...prev,
|
||||
pricing: prev.pricing.map((tier, i) =>
|
||||
i === index
|
||||
? { ...tier, [name]: value === "" ? 0 : Number(value) }
|
||||
? { ...tier, [name]: value === "" ? 0 : parseFloat(value) || 0 }
|
||||
: tier
|
||||
),
|
||||
}));
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "my-v0-project",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "my-v0-project",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"commitHash": "74b7aa4",
|
||||
"buildTime": "2025-09-23T12:09:08.230Z"
|
||||
"commitHash": "32bf9d7",
|
||||
"buildTime": "2025-10-09T19:56:57.229Z"
|
||||
}
|
||||
Reference in New Issue
Block a user