Points now subtract from costs
This commit is contained in:
@@ -87,6 +87,7 @@ export default function CartPage() {
|
||||
removeItem,
|
||||
clearCart,
|
||||
applyCoupon,
|
||||
applyPoints,
|
||||
removeCoupon,
|
||||
checkout,
|
||||
cancelCheckout,
|
||||
@@ -98,6 +99,10 @@ export default function CartPage() {
|
||||
const [couponSuccess, setCouponSuccess] = useState(null);
|
||||
const [couponLoading, setCouponLoading] = useState(false);
|
||||
|
||||
const [pointsLoading, setPointsLoading] = useState(false);
|
||||
const [pointsError, setPointsError] = useState(null);
|
||||
const [optimisticPointsApplied, setOptimisticPointsApplied] = useState(null);
|
||||
|
||||
const [checkoutLoading, setCheckoutLoading] = useState(false);
|
||||
const [checkoutError, setCheckoutError] = useState(null);
|
||||
const [clientSecret, setClientSecret] = useState(null);
|
||||
@@ -118,6 +123,8 @@ export default function CartPage() {
|
||||
cart.items.forEach((i) => (map[i.cartItemId] = i.quantity));
|
||||
setLocalQuantities(map);
|
||||
}
|
||||
// Sync optimistic state back to server truth whenever cart updates
|
||||
setOptimisticPointsApplied(null);
|
||||
}, [cart]);
|
||||
|
||||
// If the cart arrives already locked (e.g. user closed the page mid-checkout)
|
||||
@@ -184,6 +191,20 @@ export default function CartPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTogglePoints(checked) {
|
||||
setOptimisticPointsApplied(checked);
|
||||
setPointsLoading(true);
|
||||
setPointsError(null);
|
||||
try {
|
||||
await applyPoints(checked);
|
||||
} catch (err) {
|
||||
setOptimisticPointsApplied(null);
|
||||
setPointsError(err.message || "Failed to apply loyalty points.");
|
||||
} finally {
|
||||
setPointsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveCoupon() {
|
||||
setCouponLoading(true);
|
||||
setCouponError(null);
|
||||
@@ -213,6 +234,7 @@ export default function CartPage() {
|
||||
}
|
||||
|
||||
else if (result?.status === "succeeded") {
|
||||
refreshUser().catch(() => {});
|
||||
setConfirmed(true);
|
||||
}
|
||||
}
|
||||
@@ -341,7 +363,7 @@ export default function CartPage() {
|
||||
{parseFloat(cart.discountAmount ?? 0) > 0 && (
|
||||
<div className="cart-summary-row cart-summary-discount">
|
||||
<span>
|
||||
Discount
|
||||
Coupon discount
|
||||
{cart.couponCode && ` (${cart.couponCode}`}
|
||||
{(() => {
|
||||
const t = cart.couponDiscountType?.toUpperCase();
|
||||
@@ -356,15 +378,21 @@ export default function CartPage() {
|
||||
<span>−${parseFloat(cart.discountAmount).toFixed(2)}</span>
|
||||
</div>
|
||||
)}
|
||||
{parseFloat(cart.pointsDiscountAmount ?? 0) > 0 && (
|
||||
<div className="cart-summary-row cart-summary-discount">
|
||||
<span>Loyalty discount ({Math.round(parseFloat(cart.pointsDiscountAmount) * 20)} pts)</span>
|
||||
<span>−${parseFloat(cart.pointsDiscountAmount).toFixed(2)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="cart-summary-row cart-summary-total">
|
||||
<span>Total</span>
|
||||
<div className="cart-total-prices">
|
||||
{parseFloat(cart.discountAmount ?? 0) > 0 && (
|
||||
{(parseFloat(cart.discountAmount ?? 0) > 0 || parseFloat(cart.pointsDiscountAmount ?? 0) > 0) && (
|
||||
<span className="cart-total-original">
|
||||
${parseFloat(cart.subtotalAmount ?? 0).toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
<span className={parseFloat(cart.discountAmount ?? 0) > 0 ? "cart-total-discounted" : ""}>
|
||||
<span className={(parseFloat(cart.discountAmount ?? 0) > 0 || parseFloat(cart.pointsDiscountAmount ?? 0) > 0) ? "cart-total-discounted" : ""}>
|
||||
${parseFloat(cart.totalAmount ?? 0).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -382,6 +410,36 @@ export default function CartPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user?.role === "CUSTOMER" && (
|
||||
<div className="cart-points-section">
|
||||
<div className="cart-points-balance-row">
|
||||
<span>Your points balance:</span>
|
||||
<strong>{cart.availableLoyaltyPoints ?? 0} pts</strong>
|
||||
</div>
|
||||
{(cart.availableLoyaltyPoints ?? 0) < 20 ? (
|
||||
<p className="cart-points-msg">You need at least 20 points to redeem $1.</p>
|
||||
) : (
|
||||
<label className="cart-points-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cart-points-checkbox"
|
||||
checked={optimisticPointsApplied !== null ? optimisticPointsApplied : !!cart.pointsApplied}
|
||||
disabled={pointsLoading}
|
||||
onChange={(e) => handleTogglePoints(e.target.checked)}
|
||||
/>
|
||||
Use loyalty points for this purchase
|
||||
</label>
|
||||
)}
|
||||
{pointsError && <p className="cart-points-msg" style={{ color: "#dc2626" }}>{pointsError}</p>}
|
||||
{(optimisticPointsApplied ?? !!cart.pointsApplied) && parseFloat(cart.pointsDiscountAmount ?? 0) > 0 && (
|
||||
<div className="cart-points-applied-detail">
|
||||
<span>Applying {Math.round(parseFloat(cart.pointsDiscountAmount) * 20)} pts:</span>
|
||||
<span>${parseFloat(cart.pointsDiscountAmount).toFixed(2)} off</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="cart-coupon-section">
|
||||
{cart.couponCode && (
|
||||
<div className="cart-coupon-applied">
|
||||
|
||||
Reference in New Issue
Block a user