diff --git a/apps/spa/src/pages/CheckoutPage.tsx b/apps/spa/src/pages/CheckoutPage.tsx index 7d80516..53684f6 100644 --- a/apps/spa/src/pages/CheckoutPage.tsx +++ b/apps/spa/src/pages/CheckoutPage.tsx @@ -24,6 +24,11 @@ export default function CheckoutPage() { const [couponLoading, setCouponLoading] = useState(false) const [couponError, setCouponError] = useState('') + const [activePromos, setActivePromos] = useState([]) + const [customerPoints, setCustomerPoints] = useState(0) + const [usePoints, setUsePoints] = useState(false) + const [pointsToUse, setPointsToUse] = useState(0) + const [formData, setFormData] = useState({ name: '', email: '', @@ -57,6 +62,13 @@ export default function CheckoutPage() { } }) .catch(console.error) + + fetch(`${API_BASE}/customers/${customer.id}`) + .then(res => res.json()) + .then(data => { + if (data && data.points) setCustomerPoints(data.points) + }) + .catch(console.error) } }, [customer]) @@ -74,6 +86,13 @@ export default function CheckoutPage() { .then(res => res.json()) .then(setCategories) + fetch(`${API_BASE}/promotions/active`) + .then(res => res.json()) + .then(data => { + if (Array.isArray(data)) setActivePromos(data) + }) + .catch(console.error) + // Check if there's an active coupon saved in localStorage const savedCoupon = localStorage.getItem('homaroma_active_coupon') if (savedCoupon) { @@ -92,8 +111,33 @@ export default function CheckoutPage() { }, [items, navigate]) const subtotal = getCartTotal() + + // Calculate Level 1 Promo + const promoLevel1 = activePromos.find(p => p.level === 1 && subtotal >= p.minPurchase) + const promoLevel1Disc = promoLevel1 + ? (promoLevel1.type === 'PERCENTAGE' ? subtotal * (promoLevel1.value / 100) : promoLevel1.value) + : 0 + + const subtotalAfterL1 = Math.max(0, subtotal - promoLevel1Disc) + + // Calculate Level 2 Promo + const promoLevel2 = activePromos.find(p => p.level === 2 && subtotalAfterL1 >= p.minPurchase) + const promoLevel2Disc = promoLevel2 + ? (promoLevel2.type === 'PERCENTAGE' ? subtotalAfterL1 * (promoLevel2.value / 100) : promoLevel2.value) + : 0 + + const subtotalAfterL2 = Math.max(0, subtotalAfterL1 - promoLevel2Disc) + const effectiveShippingFee = isFreeShipping ? 0 : shippingFee - const total = Math.max(0, subtotal + effectiveShippingFee - discountAmount) + + // After Coupon (L3) + const subtotalAfterL3 = Math.max(0, subtotalAfterL2 - discountAmount) + + // Points Deduction (Max points to use = Math.min(customerPoints, subtotalAfterL3)) + const maxPointsAllowed = Math.floor(subtotalAfterL3) + const pointsDiscount = usePoints ? Math.min(pointsToUse, maxPointsAllowed) : 0 + + const total = Math.max(0, subtotalAfterL3 - pointsDiscount + effectiveShippingFee) // Auto-apply saved coupon if subtotal is ready useEffect(() => { @@ -126,7 +170,7 @@ export default function CheckoutPage() { fetch(`${API_BASE}/coupons/validate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code: appliedCoupon, cartTotal: subtotal }) + body: JSON.stringify({ code: appliedCoupon, cartTotal: subtotalAfterL2 }) }) .then(r => r.json()) .then(data => { @@ -150,7 +194,7 @@ export default function CheckoutPage() { const res = await fetch(`${API_BASE}/coupons/validate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code: couponCode, cartTotal: subtotal }) + body: JSON.stringify({ code: couponCode, cartTotal: subtotalAfterL2 }) }) const data = await res.json() @@ -193,8 +237,15 @@ export default function CheckoutPage() { shippingAddress: formData.address, totalAmount: total, shippingFee: effectiveShippingFee, + subtotal, + promoLevel1Name: promoLevel1?.name || null, + promoLevel1Disc: promoLevel1Disc || 0, + promoLevel2Name: promoLevel2?.name || null, + promoLevel2Disc: promoLevel2Disc || 0, couponCode: appliedCoupon || undefined, discountAmount: discountAmount || undefined, + pointsUsed: pointsDiscount > 0 ? pointsDiscount : 0, + pointsDiscount: pointsDiscount > 0 ? pointsDiscount : 0, items: items.map(item => ({ productId: item.productId, variantId: item.variantId, @@ -346,6 +397,44 @@ export default function CheckoutPage() { )} + + {/* Points Section */} + {customerPoints > 0 && ( +
+
+ + {customerPoints} Points Available +
+
+ { + setUsePoints(e.target.checked) + if (e.target.checked) setPointsToUse(Math.min(customerPoints, maxPointsAllowed)) + }} + className="w-4 h-4 text-spa-forest border-spa-stone/20 rounded focus:ring-spa-forest" + /> + +
+ {usePoints && ( +
+ setPointsToUse(Math.min(Number(e.target.value), customerPoints, maxPointsAllowed))} + className="w-24 bg-spa-light border border-spa-stone/20 rounded-lg px-3 py-1.5 text-sm text-pop-black focus:border-spa-forest outline-none" + /> + Points (-฿{pointsToUse}) +
+ )} +
+ )}
{items.map(item => ( @@ -376,12 +465,30 @@ export default function CheckoutPage() { - {formatPrice(shippingFee)}
)} + {promoLevel1Disc > 0 && ( +
+ {promoLevel1?.name} + - {formatPrice(promoLevel1Disc)} +
+ )} + {promoLevel2Disc > 0 && ( +
+ {promoLevel2?.name} + - {formatPrice(promoLevel2Disc)} +
+ )} {discountAmount > 0 && (
- Discount + Coupon ({appliedCoupon}) - {formatPrice(discountAmount)}
)} + {pointsDiscount > 0 && ( +
+ Points Redeemed + - {formatPrice(pointsDiscount)} +
+ )}
Total {formatPrice(total)} diff --git a/apps/spa/src/store/authStore.ts b/apps/spa/src/store/authStore.ts index c0cec90..629f11c 100644 --- a/apps/spa/src/store/authStore.ts +++ b/apps/spa/src/store/authStore.ts @@ -8,6 +8,7 @@ export interface Customer { pictureUrl: string | null; email: string | null; phone: string | null; + points?: number; } interface AuthState {