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