From 7d029efe1ba4736dd5fb80454f1825d9dfe0dba5 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Wed, 29 Apr 2026 20:00:58 +0200 Subject: [PATCH] refactor: update shipping fee logic to use threshold-based calculation and improve piece count handling --- .../hooks/useShippingFees.ts | 23 ++-- src/app/coffee-abonnements/page.tsx | 116 ++++++++---------- src/app/coffee-abonnements/summary/page.tsx | 14 +-- src/app/profile/subscriptions/page.tsx | 6 +- 4 files changed, 72 insertions(+), 87 deletions(-) diff --git a/src/app/coffee-abonnements/hooks/useShippingFees.ts b/src/app/coffee-abonnements/hooks/useShippingFees.ts index 18a59eb..84377d0 100644 --- a/src/app/coffee-abonnements/hooks/useShippingFees.ts +++ b/src/app/coffee-abonnements/hooks/useShippingFees.ts @@ -2,18 +2,14 @@ import { useEffect, useMemo, useState } from 'react'; -export type CoffeeShippingFeePieceCount = 60 | 120; - export type CoffeeShippingFee = { - pieceCount: CoffeeShippingFeePieceCount; + pieceCount: number; price: number; }; -type ShippingFeeMap = Record; - -function normalizePieceCount(v: any): CoffeeShippingFeePieceCount | null { +function normalizePieceCount(v: any): number | null { const n = typeof v === 'number' ? v : (typeof v === 'string' && /^\d+$/.test(v) ? Number(v) : NaN); - if (n === 60 || n === 120) return n; + if (Number.isInteger(n) && n >= 60 && n % 10 === 0) return n; return null; } @@ -24,12 +20,11 @@ export function useShippingFees() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const feeByPieceCount: ShippingFeeMap = useMemo(() => { - const map: ShippingFeeMap = { 60: 0, 120: 0 }; - for (const row of fees) { - map[row.pieceCount] = row.price; - } - return map; + // Threshold lookup: finds the highest breakpoint <= n (mirrors backend logic). + const resolveShippingFee = useMemo(() => (n: number): number => { + const sorted = [...fees].sort((a, b) => b.pieceCount - a.pieceCount); + const match = sorted.find(f => f.pieceCount <= n); + return match ? match.price : 0; }, [fees]); useEffect(() => { @@ -83,5 +78,5 @@ export function useShippingFees() { }; }, [base]); - return { fees, feeByPieceCount, loading, error }; + return { fees, resolveShippingFee, loading, error }; } diff --git a/src/app/coffee-abonnements/page.tsx b/src/app/coffee-abonnements/page.tsx index 885730d..9f8d61a 100644 --- a/src/app/coffee-abonnements/page.tsx +++ b/src/app/coffee-abonnements/page.tsx @@ -8,19 +8,41 @@ import { useShippingFees } from './hooks/useShippingFees'; export default function CoffeeAbonnementPage() { const [selections, setSelections] = useState>({}); const [bump, setBump] = useState>({}); - const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<60 | 120>(120); + const [selectedPlanCapsules, setSelectedPlanCapsules] = useState(60); const router = useRouter(); // Fetch active coffees from the backend const { coffees, loading, error } = useActiveCoffees(); - // Shipping fees (per piece count) - const { feeByPieceCount, loading: shippingLoading, error: shippingError } = useShippingFees(); - const shippingFeeFor60 = feeByPieceCount[60] ?? 0; - const shippingFeeFor120 = feeByPieceCount[120] ?? 0; - const selectedShippingFee = feeByPieceCount[selectedPlanCapsules] ?? 0; + // Shipping fees (threshold-based) + const { resolveShippingFee, loading: shippingLoading, error: shippingError } = useShippingFees(); + const selectedShippingFee = resolveShippingFee(selectedPlanCapsules); const isFreeShippingSelected = Number(selectedShippingFee) === 0; + const changePlanSize = (delta: number) => { + setSelectedPlanCapsules(prev => { + const next = Math.max(60, prev + delta); + // Trim selections that exceed the new plan size + setSelections(sel => { + const trimmed = { ...sel }; + let running = 0; + for (const id of Object.keys(trimmed)) { + if (running + trimmed[id] <= next) { + running += trimmed[id]; + } else if (running < next) { + const allowed = Math.floor((next - running) / 10) * 10; + if (allowed >= 10) { trimmed[id] = allowed; running = next; } + else delete trimmed[id]; + } else { + delete trimmed[id]; + } + } + return trimmed; + }); + return next; + }); + }; + const selectedEntries = useMemo( () => Object.entries(selections).map(([id, qty]) => { @@ -81,7 +103,7 @@ export default function CoffeeAbonnementPage() { setSelections((prev) => { if (!(id in prev)) return prev; const otherTotal = Object.entries(prev).reduce((sum, [key, qty]) => key === id ? sum : sum + qty, 0); - const maxForCoffee = Math.min(120, selectedPlanCapsules - otherTotal); + const maxForCoffee = selectedPlanCapsules - otherTotal; const next = prev[id] + delta; if (next < 10 || next > maxForCoffee) return prev; const updated = { ...prev, [id]: next }; @@ -115,63 +137,32 @@ export default function CoffeeAbonnementPage() {

1. Select subscription size

-
+
+ onClick={() => changePlanSize(-10)} + disabled={selectedPlanCapsules <= 60} + className="h-10 w-10 rounded-full bg-gray-100 hover:bg-gray-200 text-lg font-bold transition disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center" + >− +
+
{selectedPlanCapsules} pcs
+
{selectedPlanCapsules / 10} packs of 10 · min. 60
+
+ onClick={() => changePlanSize(+10)} + className="h-10 w-10 rounded-full bg-gray-100 hover:bg-gray-200 text-lg font-bold transition flex items-center justify-center" + >+ +
+ {shippingLoading ? ( + Shipping… + ) : isFreeShippingSelected ? ( + FREE SHIPPING + ) : ( + Shipping €{selectedShippingFee.toFixed(2)} + )} +
- {shippingError && (
Shipping fees could not be loaded: {shippingError} @@ -263,7 +254,7 @@ export default function CoffeeAbonnementPage() { {active && (
- Quantity (10–120) + Quantity (10–{maxForCoffee} pcs) @@ -374,8 +365,7 @@ export default function CoffeeAbonnementPage() { Selected: {totalCapsules} capsules ({packsSelected} packs of 10). Target: {selectedPlanCapsules} capsules ({requiredPacks} packs). {packsSelected !== requiredPacks && ( - Please select exactly {selectedPlanCapsules} capsules ({requiredPacks} packs). - {packsSelected < requiredPacks ? ` ${requiredPacks - packsSelected} packs missing.` : ` ${packsSelected - requiredPacks} packs too many.`} + {packsSelected < requiredPacks ? `${requiredPacks - packsSelected} packs missing.` : `${packsSelected - requiredPacks} packs too many — reduce plan size or remove some coffees.`} )}
@@ -407,7 +397,7 @@ export default function CoffeeAbonnementPage() { {!canProceed && (

- You can continue once exactly {selectedPlanCapsules} capsules ({requiredPacks} packs) are selected. + You can continue once exactly {selectedPlanCapsules} capsules ({requiredPacks} packs) are selected. Use the +/− buttons above to adjust the plan size.

)}
diff --git a/src/app/coffee-abonnements/summary/page.tsx b/src/app/coffee-abonnements/summary/page.tsx index f87ad1c..ce479c9 100644 --- a/src/app/coffee-abonnements/summary/page.tsx +++ b/src/app/coffee-abonnements/summary/page.tsx @@ -47,7 +47,7 @@ export default function SummaryPage() { const router = useRouter(); const { coffees, loading, error } = useActiveCoffees(); const user = useAuthStore(state => state.user) - const { feeByPieceCount, loading: shippingLoading, error: shippingError } = useShippingFees(); + const { resolveShippingFee, loading: shippingLoading, error: shippingError } = useShippingFees(); const { html: contractHtml, loading: contractLoading, error: contractError } = useAboContractTemplateHtml() const [isContractPreviewOpen, setIsContractPreviewOpen] = useState(false) const [contractPdfUrl, setContractPdfUrl] = useState('') @@ -55,7 +55,7 @@ export default function SummaryPage() { const [contractPdfLoading, setContractPdfLoading] = useState(false) const [contractPdfError, setContractPdfError] = useState(null) const [selections, setSelections] = useState>({}); - const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<60 | 120>(120); + const [selectedPlanCapsules, setSelectedPlanCapsules] = useState(60); const [signatureDataUrl, setSignatureDataUrl] = useState('') const [form, setForm] = useState({ firstName: '', @@ -349,8 +349,9 @@ export default function SummaryPage() { const raw = sessionStorage.getItem('coffeeSelections'); if (raw) setSelections(JSON.parse(raw)); const rawPlan = sessionStorage.getItem('coffeeAboSizeCapsules'); - if (rawPlan === '60' || rawPlan === '120') { - setSelectedPlanCapsules(Number(rawPlan) as 60 | 120); + const parsedPlan = rawPlan ? Number(rawPlan) : null; + if (parsedPlan && Number.isInteger(parsedPlan) && parsedPlan >= 60 && parsedPlan % 10 === 0) { + setSelectedPlanCapsules(parsedPlan); } } catch {} }, []); @@ -449,9 +450,8 @@ export default function SummaryPage() { ); const shippingFee = useMemo(() => { - const v = feeByPieceCount[selectedPlanCapsules]; - return Number.isFinite(Number(v)) ? Number(v) : 0; - }, [feeByPieceCount, selectedPlanCapsules]); + return resolveShippingFee(selectedPlanCapsules); + }, [resolveShippingFee, selectedPlanCapsules]); const netWithShipping = useMemo( () => totalPrice + shippingFee, diff --git a/src/app/profile/subscriptions/page.tsx b/src/app/profile/subscriptions/page.tsx index 62dca15..1aa2299 100644 --- a/src/app/profile/subscriptions/page.tsx +++ b/src/app/profile/subscriptions/page.tsx @@ -153,8 +153,8 @@ export default function ProfileSubscriptionsPage() { setContentError('Please select at least one coffee with quantity greater than 0.') return } - if (draftTotalPacks !== 6 && draftTotalPacks !== 12) { - setContentError('Total packs must be exactly 6 or 12.') + if (draftTotalPacks < 6) { + setContentError('Total must be at least 6 packs (60 capsules).') return } @@ -439,7 +439,7 @@ export default function ProfileSubscriptionsPage() {

Edit coffee content

-

Selected packs: {draftTotalPacks} (must be 6 or 12)

+

Selected packs: {draftTotalPacks} (minimum 6)

{coffeesLoading ? (