@@ -130,7 +137,7 @@ export default function CoffeeAbonnementPage() {
{/* price badge (per 10) */}
- pro 10 Stk
+ per 10 pcs
@@ -157,16 +164,16 @@ export default function CoffeeAbonnementPage() {
: 'border-gray-300 hover:bg-gray-100'
}`}
>
- {active ? 'Entfernen' : 'Hinzufügen'}
+ {active ? 'Remove' : 'Add'}
{active && (
- Menge (10–120)
+ Quantity (10–120)
- {qty} Stk
+ {qty} pcs
@@ -223,10 +230,10 @@ export default function CoffeeAbonnementPage() {
{/* Section 2: Compact preview + next steps */}
- 2. Vorschau
+ 2. Preview
{selectedEntries.length === 0 && (
-
Noch keine Kaffees ausgewählt.
+
No coffees selected yet.
)}
{selectedEntries.map((entry) => (
@@ -245,11 +252,23 @@ export default function CoffeeAbonnementPage() {
))}
- Gesamt (Netto)
+ Total (net)
€{totalPrice.toFixed(2)}
+
+ {/* Packs/capsules summary and validation hint (refined design) */}
+
+ Selected: {totalCapsules} capsules ({packsSelected} packs of 10).
+ {packsSelected !== 12 && (
+
+ Please select exactly 120 capsules (12 packs).
+ {packsSelected < 12 ? ` ${12 - packsSelected} packs missing.` : ` ${packsSelected - 12} packs too many.`}
+
+ )}
+
+
{!canProceed && (
-
Bitte mindestens eine Kaffeesorte wählen.
+
+ You can continue once exactly 120 capsules (12 packs) are selected.
+
)}
diff --git a/src/app/coffee-abonnements/summary/hooks/getTaxRate.ts b/src/app/coffee-abonnements/summary/hooks/getTaxRate.ts
index 937a76c..4c712ac 100644
--- a/src/app/coffee-abonnements/summary/hooks/getTaxRate.ts
+++ b/src/app/coffee-abonnements/summary/hooks/getTaxRate.ts
@@ -10,22 +10,83 @@ const normalizeVatRate = (rate: number | null | undefined): number | null => {
return rate > 1 ? rate / 100 : rate;
};
+const toNumber = (v: any): number | null => {
+ if (v == null) return null;
+ const n = typeof v === 'string' ? Number(v) : (typeof v === 'number' ? v : Number(v));
+ return Number.isFinite(n) ? n : null;
+};
+
+const getCode = (row: any): string => {
+ const raw = row?.countryCode ?? row?.code ?? row?.country ?? row?.country_code;
+ return typeof raw === 'string' ? raw.toUpperCase() : '';
+};
+
+const getRateRaw = (row: any): number | null => {
+ // support multiple field names and string numbers
+ const raw = row?.standardRate ?? row?.rate ?? row?.ratePercent ?? row?.standard_rate;
+ const num = toNumber(raw);
+ return num;
+};
+
+const getRateNormalized = (row: any): number | null => {
+ const num = getRateRaw(row);
+ return normalizeVatRate(num);
+};
+
/**
* Fetches the standard VAT rate for a given ISO country code.
* Returns null if not found or on any error.
*/
export async function getStandardVatRate(countryCode: string): Promise
{
try {
- const res = await authFetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/tax/vat-rates`, { method: "GET" });
+ const url = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/tax/vat-rates`;
+ console.info('[VAT] getStandardVatRate -> GET', url, { countryCode });
+ const res = await authFetch(url, { method: "GET" });
+ console.info('[VAT] getStandardVatRate status:', res.status);
if (!res.ok) return null;
- const data = await res.json().catch(() => null);
- if (!data || !Array.isArray(data)) return null;
+ const raw = await res.json().catch(() => null);
+ const arr = Array.isArray(raw?.data) ? raw.data : (Array.isArray(raw) ? raw : []);
+ console.info('[VAT] getStandardVatRate parsed length:', Array.isArray(arr) ? arr.length : 0);
+ if (!Array.isArray(arr) || arr.length === 0) return null;
const upper = countryCode.toUpperCase();
- const match = (data as VatRate[]).find(r => r.countryCode.toUpperCase() === upper);
- return normalizeVatRate(match?.standardRate);
- } catch {
+ const match = arr.find((r: any) => getCode(r) === upper);
+ const normalized = match ? getRateNormalized(match) : null;
+ console.info('[VAT] getStandardVatRate match:', {
+ upper,
+ resolvedCode: match ? getCode(match) : null,
+ rawRate: match ? getRateRaw(match) : null,
+ normalized
+ });
+ return normalized;
+ } catch (e) {
+ console.error('[VAT] getStandardVatRate error:', e);
return null;
}
}
+
+export type VatRateEntry = { code: string; rate: number | null }
+
+export async function getVatRates(): Promise {
+ try {
+ const url = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/tax/vat-rates`;
+ console.info('[VAT] getVatRates -> GET', url);
+ const res = await authFetch(url, { method: 'GET' });
+ console.info('[VAT] getVatRates status:', res.status);
+ if (!res.ok) return [];
+ const raw = await res.json().catch(() => null);
+ const arr = Array.isArray(raw?.data) ? raw.data : (Array.isArray(raw) ? raw : []);
+ console.info('[VAT] getVatRates parsed length:', Array.isArray(arr) ? arr.length : 0);
+ if (!Array.isArray(arr) || arr.length === 0) return [];
+ const mapped = arr.map((r: any) => ({
+ code: getCode(r),
+ rate: getRateNormalized(r)
+ })).filter((r: VatRateEntry) => !!r.code);
+ console.info('[VAT] getVatRates mapped sample:', mapped.slice(0, 5));
+ return mapped;
+ } catch (e) {
+ console.error('[VAT] getVatRates error:', e);
+ return [];
+ }
+}
diff --git a/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts b/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts
new file mode 100644
index 0000000..efffbb8
--- /dev/null
+++ b/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts
@@ -0,0 +1,138 @@
+import { authFetch } from '../../../utils/authFetch'
+
+export type SubscribeAboItem = { coffeeId: string | number; quantity?: number }
+export type SubscribeAboInput = {
+ coffeeId?: string | number // optional when items provided
+ items?: SubscribeAboItem[] // NEW: whole order in one call
+ billing_interval?: string
+ interval_count?: number
+ is_auto_renew?: boolean
+ target_user_id?: number
+ recipient_name?: string
+ recipient_email?: string
+ recipient_notes?: string
+ // NEW: customer fields
+ firstName?: string
+ lastName?: string
+ email?: string
+ street?: string
+ postalCode?: string
+ city?: string
+ country?: string
+ frequency?: string
+ startDate?: string
+ // NEW: logged-in user id
+ referred_by?: number
+}
+
+type Abonement = any
+type HistoryEvent = any
+
+const apiBase = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '')
+
+const parseJson = async (res: Response) => {
+ const ct = res.headers.get('content-type') || ''
+ const isJson = ct.includes('application/json')
+ const json = isJson ? await res.json().catch(() => ({})) : null
+ return { json, isJson }
+}
+
+export async function subscribeAbo(input: SubscribeAboInput) {
+ const hasItems = Array.isArray(input.items) && input.items.length > 0
+ if (!hasItems && !input.coffeeId) throw new Error('coffeeId is required')
+
+ const hasRecipientFields = !!(input.recipient_name || input.recipient_email || input.recipient_notes)
+ if (hasRecipientFields && !input.recipient_name) {
+ throw new Error('recipient_name is required when gifting to a non-account recipient.')
+ }
+
+ // NEW: validate customer fields (required in UI)
+ const requiredFields = ['firstName','lastName','email','street','postalCode','city','country','frequency','startDate'] as const
+ const missing = requiredFields.filter(k => {
+ const v = (input as any)[k]
+ return typeof v !== 'string' || v.trim() === ''
+ })
+ if (missing.length) {
+ throw new Error(`Missing required fields: ${missing.join(', ')}`)
+ }
+
+ const body: any = {
+ billing_interval: input.billing_interval ?? 'month',
+ interval_count: input.interval_count ?? 1,
+ is_auto_renew: input.is_auto_renew ?? true,
+ // NEW: include customer fields
+ firstName: input.firstName,
+ lastName: input.lastName,
+ email: input.email,
+ street: input.street,
+ postalCode: input.postalCode,
+ city: input.city,
+ country: input.country?.toUpperCase?.() ?? input.country,
+ frequency: input.frequency,
+ startDate: input.startDate,
+ }
+ if (hasItems) {
+ body.items = input.items!.map(i => ({
+ coffeeId: i.coffeeId,
+ quantity: i.quantity != null ? i.quantity : 1,
+ }))
+ // NEW: enforce exactly 12 packs
+ const sumPacks = body.items.reduce((s: number, it: any) => s + Number(it.quantity || 0), 0)
+ if (sumPacks !== 12) {
+ console.warn('[subscribeAbo] Invalid pack total:', sumPacks, 'expected 12')
+ throw new Error('Order must contain exactly 12 packs (120 capsules).')
+ }
+ } else {
+ body.coffeeId = input.coffeeId
+ // single-item legacy path — backend expects bundle, prefer items usage
+ }
+ // NEW: always include available recipient fields and target_user_id when provided
+ if (input.target_user_id != null) body.target_user_id = input.target_user_id
+ if (input.recipient_name) body.recipient_name = input.recipient_name
+ if (input.recipient_email) body.recipient_email = input.recipient_email
+ if (input.recipient_notes) body.recipient_notes = input.recipient_notes
+ // NEW: always include referred_by if provided
+ if (input.referred_by != null) body.referred_by = input.referred_by
+
+ const url = `${apiBase}/api/abonements/subscribe`
+ console.info('[subscribeAbo] POST', url, { body })
+ // NEW: explicit JSON preview that matches the actual request body
+ console.info('[subscribeAbo] Body JSON:', JSON.stringify(body))
+ const res = await authFetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
+ body: JSON.stringify(body),
+ credentials: 'include',
+ })
+ const { json } = await parseJson(res)
+ console.info('[subscribeAbo] Response', res.status, json)
+ if (!res.ok || !json?.success) throw new Error(json?.message || `Subscribe failed: ${res.status}`)
+ return json.data as Abonement
+}
+
+async function postAction(url: string) {
+ const res = await authFetch(url, { method: 'POST', headers: { Accept: 'application/json' }, credentials: 'include' })
+ const { json } = await parseJson(res)
+ if (!res.ok || !json?.success) throw new Error(json?.message || `Request failed: ${res.status}`)
+ return json.data as Abonement
+}
+
+export const pauseAbo = (id: string | number) => postAction(`${apiBase}/abonements/${id}/pause`)
+export const resumeAbo = (id: string | number) => postAction(`${apiBase}/abonements/${id}/resume`)
+export const cancelAbo = (id: string | number) => postAction(`${apiBase}/abonements/${id}/cancel`)
+export const renewAbo = (id: string | number) => postAction(`${apiBase}/admin/abonements/${id}/renew`)
+
+export async function getMyAbonements(status?: string) {
+ const qs = status ? `?status=${encodeURIComponent(status)}` : ''
+ const res = await authFetch(`${apiBase}/abonements/mine${qs}`, { method: 'GET', headers: { Accept: 'application/json' }, credentials: 'include' })
+ const { json } = await parseJson(res)
+ if (!res.ok || !json?.success) throw new Error(json?.message || `Fetch failed: ${res.status}`)
+ return (json.data || []) as Abonement[]
+}
+
+export async function getAboHistory(id: string | number) {
+ const res = await authFetch(`${apiBase}/abonements/${id}/history`, { method: 'GET', headers: { Accept: 'application/json' }, credentials: 'include' })
+ const { json } = await parseJson(res)
+ if (!res.ok || !json?.success) throw new Error(json?.message || `History failed: ${res.status}`)
+ return (json.data || []) as HistoryEvent[]
+}
diff --git a/src/app/coffee-abonnements/summary/page.tsx b/src/app/coffee-abonnements/summary/page.tsx
index 052eb22..f405e9f 100644
--- a/src/app/coffee-abonnements/summary/page.tsx
+++ b/src/app/coffee-abonnements/summary/page.tsx
@@ -3,15 +3,9 @@ import React, { useEffect, useMemo, useState } from 'react';
import PageLayout from '../../components/PageLayout';
import { useRouter } from 'next/navigation';
import { useActiveCoffees } from '../hooks/getActiveCoffees';
-import { getStandardVatRate } from './hooks/getTaxRate';
-
-const FALLBACK_RATES: Record = {
- DE: 0.19,
- AT: 0.20,
- CH: 0.077,
- FR: 0.20,
- NL: 0.21,
-};
+import { getStandardVatRate, getVatRates } from './hooks/getTaxRate';
+import { subscribeAbo } from './hooks/subscribeAbo';
+import useAuthStore from '../../store/authStore'
export default function SummaryPage() {
const router = useRouter();
@@ -30,7 +24,10 @@ export default function SummaryPage() {
});
const [showThanks, setShowThanks] = useState(false);
const [confetti, setConfetti] = useState<{ left: number; delay: number; color: string }[]>([]);
- const [taxRate, setTaxRate] = useState(FALLBACK_RATES['DE'] ?? 0.07);
+ const [taxRate, setTaxRate] = useState(0.07); // minimal fallback only
+ const [vatRates, setVatRates] = useState<{ code: string; rate: number | null }[]>([]);
+ const [submitError, setSubmitError] = useState(null);
+ const [submitLoading, setSubmitLoading] = useState(false);
const COLORS = ['#1C2B4A', '#233357', '#2A3B66', '#314475', '#3A4F88', '#5B6C9A']; // dark blue palette
useEffect(() => {
@@ -61,24 +58,74 @@ export default function SummaryPage() {
[selections, coffees]
);
+ // NEW: computed packs/capsules for validation
+ const totalCapsules = useMemo(
+ () => selectedEntries.reduce((sum, e) => sum + e.quantity, 0),
+ [selectedEntries]
+ )
+ const totalPacks = totalCapsules / 10
+
+ const token = useAuthStore.getState().accessToken
+ console.info('[SummaryPage] token prefix:', token ? `${token.substring(0, 12)}…` : null)
+ // NEW: capture logged-in user id for referral
+ const currentUserId = useAuthStore.getState().user?.id
+ console.info('[SummaryPage] currentUserId:', currentUserId)
+
+ // Countries list from backend VAT rates (fallback to current country if list empty)
+ const countryOptions = useMemo(() => {
+ const opts = vatRates.length > 0 ? vatRates.map(r => r.code) : [(form.country || 'DE').toUpperCase()]
+ console.info('[SummaryPage] countryOptions:', opts)
+ return opts
+ }, [vatRates, form.country]);
+
+ // Load VAT rates list from backend and set initial taxRate
useEffect(() => {
let active = true;
(async () => {
- const fallback = FALLBACK_RATES[form.country] ?? 0.07;
- const rate = await getStandardVatRate(form.country);
- if (active) setTaxRate(rate ?? fallback);
+ console.info('[SummaryPage] Loading vat rates (mount). country:', form.country)
+ const list = await getVatRates();
+ if (!active) return;
+ console.info('[SummaryPage] getVatRates result count:', list.length)
+ setVatRates(list);
+ const upper = form.country.toUpperCase();
+ const match = list.find(r => r.code === upper);
+ if (match?.rate != null) {
+ console.info('[SummaryPage] Initial taxRate from list:', match.rate, 'country:', upper)
+ setTaxRate(match.rate);
+ } else {
+ const rate = await getStandardVatRate(form.country);
+ console.info('[SummaryPage] Fallback taxRate via getStandardVatRate:', rate, 'country:', upper)
+ setTaxRate(rate ?? 0.07);
+ }
})();
- return () => {
- active = false;
- };
- }, [form.country]);
+ return () => { active = false; };
+ }, []); // mount-only
+
+ // Update taxRate when country changes (from backend only)
+ useEffect(() => {
+ let active = true;
+ (async () => {
+ const upper = form.country.toUpperCase();
+ console.info('[SummaryPage] Country changed:', upper)
+ const fromList = vatRates.find(r => r.code === upper)?.rate;
+ if (fromList != null) {
+ console.info('[SummaryPage] taxRate from existing list:', fromList)
+ if (active) setTaxRate(fromList);
+ return;
+ }
+ const rate = await getStandardVatRate(form.country);
+ console.info('[SummaryPage] taxRate via getStandardVatRate:', rate)
+ if (active) setTaxRate(rate ?? 0.07);
+ })();
+ return () => { active = false; };
+ }, [form.country, vatRates]);
const totalPrice = useMemo(
() => selectedEntries.reduce((sum, e) => sum + (e.quantity / 10) * e.coffee.pricePer10, 0),
[selectedEntries]
);
const taxAmount = useMemo(() => totalPrice * taxRate, [totalPrice, taxRate]);
- const totalWithTax = useMemo(() => totalPrice + taxAmount, [totalPrice, taxAmount]);
+ const totalWithTax = useMemo(() => totalPrice + taxAmount, [totalPrice, taxRate, taxAmount]);
const handleInput = (e: React.ChangeEvent) => {
const { name, value } = e.target;
@@ -87,15 +134,53 @@ export default function SummaryPage() {
const canSubmit =
selectedEntries.length > 0 &&
+ totalPacks === 12 && // CHANGED: require exactly 12 packs
Object.values(form).every(v => (typeof v === 'string' ? v.trim() !== '' : true));
const backToSelection = () => router.push('/coffee-abonnements');
- const submit = () => {
- if (!canSubmit) return;
- setShowThanks(true);
- try { sessionStorage.removeItem('coffeeSelections'); } catch {}
- // here you would post data to your API
+ const submit = async () => {
+ if (!canSubmit || submitLoading) return
+ // NEW: guard (defensive) — backend requires exactly 12 packs
+ if (totalPacks !== 12) {
+ setSubmitError('Order must contain exactly 12 packs (120 capsules).')
+ return
+ }
+ setSubmitError(null)
+ setSubmitLoading(true)
+ try {
+ const payload = {
+ items: selectedEntries.map(entry => ({
+ coffeeId: entry.coffee.id,
+ quantity: Math.round(entry.quantity / 10), // packs
+ })),
+ billing_interval: 'month',
+ interval_count: 1,
+ is_auto_renew: true,
+ // NEW: pass customer fields
+ firstName: form.firstName.trim(),
+ lastName: form.lastName.trim(),
+ email: form.email.trim(),
+ street: form.street.trim(),
+ postalCode: form.postalCode.trim(),
+ city: form.city.trim(),
+ country: form.country.trim(),
+ frequency: form.frequency.trim(),
+ startDate: form.startDate.trim(),
+ // NEW: always include referred_by if available
+ referred_by: typeof currentUserId === 'number' ? currentUserId : undefined,
+ }
+ console.info('[SummaryPage] subscribeAbo payload:', payload)
+ // NEW: explicit JSON preview to match request body
+ console.info('[SummaryPage] subscribeAbo payload JSON:', JSON.stringify(payload))
+ await subscribeAbo(payload)
+ setShowThanks(true);
+ try { sessionStorage.removeItem('coffeeSelections'); } catch {}
+ } catch (e: any) {
+ setSubmitError(e?.message || 'Subscription could not be created.');
+ } finally {
+ setSubmitLoading(false);
+ }
};
return (
@@ -103,13 +188,13 @@ export default function SummaryPage() {
- Zusammenfassung & Daten
+ Summary & Details
@@ -117,12 +202,12 @@ export default function SummaryPage() {
1
- Auswahl
+ Selection
2
- Zusammenfassung
+ Summary
@@ -133,123 +218,137 @@ export default function SummaryPage() {
onClick={backToSelection}
className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90"
>
- Zur Auswahl
+ Back to selection
)}
+ {/* submit error */}
+ {submitError && (
+
+ )}
+
{loading ? (
) : selectedEntries.length === 0 ? (
-
Keine Auswahl gefunden.
+
No selection found.
) : (
{/* Left: Customer data */}
- 1. Deine Daten
+ 1. Your details
- {!canSubmit &&
Bitte mindestens eine Kaffeesorte wählen und alle Felder ausfüllen.
}
+ {!canSubmit &&
Please select coffees and fill all fields.
}
{/* Right: Order summary */}
- 2. Deine Auswahl
+ 2. Your selection
{selectedEntries.map(entry => (
{entry.coffee.name}
- {entry.quantity} Stk • €{entry.coffee.pricePer10}/10
+ {entry.quantity} pcs • €{entry.coffee.pricePer10}/10
€{((entry.quantity / 10) * entry.coffee.pricePer10).toFixed(2)}
))}
- Gesamt (Netto)
+ Total (net)
€{totalPrice.toFixed(2)}
- Steuer ({(taxRate * 100).toFixed(1)}%)
+ Tax ({(taxRate * 100).toFixed(1)}%)
€{taxAmount.toFixed(2)}
- Gesamt inkl. Steuer
+ Total incl. tax
€{totalWithTax.toFixed(2)}
+ {/* Validation summary (refined design) */}
+
+ Selected: {totalCapsules} capsules ({totalPacks} packs of 10).
+ {totalPacks !== 12 && (
+
+ Exactly 12 packs (120 capsules) are required.
+
+ )}
+
@@ -271,15 +370,15 @@ export default function SummaryPage() {
-
Danke für dein Abo!
-
Wir haben deine Bestellung erhalten.
+
Thanks for your subscription!
+
We have received your order.
diff --git a/src/app/profile/components/userAbo.tsx b/src/app/profile/components/userAbo.tsx
new file mode 100644
index 0000000..6e5f872
--- /dev/null
+++ b/src/app/profile/components/userAbo.tsx
@@ -0,0 +1,100 @@
+import React from 'react'
+import { useReferredAbos } from '../hooks/getAbo'
+
+export default function UserAbo() {
+ const { data: abos, loading, error } = useReferredAbos()
+
+ if (loading) {
+ return (
+
+ My Subscriptions
+
+ Loading subscriptions…
+
+
+ )
+ }
+
+ if (error) {
+ return (
+
+ My Subscriptions
+
+ {error}
+
+
+ )
+ }
+
+ return (
+
+ My Subscriptions
+ {(!abos || abos.length === 0) ? (
+
+ No subscriptions yet.
+
+ ) : (
+
+ {abos.map(abo => {
+ const status = (abo.status || 'active') as 'active' | 'paused' | 'canceled'
+ const nextBilling = abo.nextBillingAt ? new Date(abo.nextBillingAt).toLocaleDateString() : '—'
+ const started = abo.startedAt ? new Date(abo.startedAt).toLocaleDateString() : '—'
+ const coffees = (abo.pack_breakdown || abo.items || []).map((it, i) => (
+
+ {/* coffee name */}
+ {it.coffeeName || `Coffee #${it.coffeeId}`}
+ {/* packs pill — CHANGED COLORS TO MATCH ACTIVE BADGE */}
+
+ {it.quantity} packs
+
+
+ ))
+ return (
+
+
+
+
{abo.name || 'Coffee Subscription'}
+
+ Next billing: {nextBilling}
+ {' • '}Frequency: {abo.frequency ?? '—'}
+ {' • '}Country: {(abo.country ?? '').toUpperCase() || '—'}
+ {' • '}Started: {started}
+
+
+
+ {status.charAt(0).toUpperCase() + status.slice(1)}
+
+
+
+
Coffees
+
+ {coffees}
+
+
+
+
+
+
+
+ )
+ })}
+
+ )}
+
+ )
+}
diff --git a/src/app/profile/hooks/getAbo.ts b/src/app/profile/hooks/getAbo.ts
new file mode 100644
index 0000000..7f9a6ff
--- /dev/null
+++ b/src/app/profile/hooks/getAbo.ts
@@ -0,0 +1,93 @@
+import { useEffect, useState } from 'react'
+import { authFetch } from '../../utils/authFetch'
+import useAuthStore from '../../store/authStore'
+
+type AbonementItem = {
+ coffeeName?: string
+ coffeeId?: string | number
+ quantity: number
+}
+
+export type ReferredAbo = {
+ id: number | string
+ status: 'active' | 'paused' | 'canceled'
+ nextBillingAt?: string | null
+ email?: string
+ price?: string | number
+ pack_breakdown?: AbonementItem[]
+ items?: AbonementItem[]
+ name?: string
+ frequency?: string
+ country?: string
+ startedAt?: string | null
+}
+
+export function useReferredAbos() {
+ const [data, setData] = useState
([])
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ let active = true
+ ;(async () => {
+ setLoading(true)
+ setError(null)
+ try {
+ const base = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, '')
+ const user = useAuthStore.getState().user
+ const userId = user?.id
+ const email = user?.email
+
+ const url = `${base}/api/abonements/referred`
+
+ console.info('[useReferredAbos] Preparing POST', url, {
+ userId: userId ?? null,
+ userEmail: email ?? null,
+ })
+
+ const res = await authFetch(url, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ userId: userId ?? null, email: email ?? null }),
+ })
+ const ct = res.headers.get('content-type') || ''
+ const isJson = ct.includes('application/json')
+ const json = isJson ? await res.json().catch(() => ({})) : null
+ console.info('[useReferredAbos] Response', res.status, json)
+
+ if (!res.ok || !json?.success) throw new Error(json?.message || `Fetch failed: ${res.status}`)
+ const list: ReferredAbo[] = Array.isArray(json.data)
+ ? json.data.map((raw: any) => ({
+ id: raw.id,
+ status: raw.status,
+ nextBillingAt: raw.next_billing_at ?? raw.nextBillingAt ?? null,
+ email: raw.email,
+ price: raw.price,
+ name: `${raw.first_name ?? ''} ${raw.last_name ?? ''}`.trim() || 'Coffee Subscription',
+ frequency: raw.frequency,
+ country: raw.country,
+ startedAt: raw.started_at ?? null,
+ pack_breakdown: Array.isArray(raw.pack_breakdown)
+ ? raw.pack_breakdown.map((it: any) => ({
+ coffeeName: it.coffee_name,
+ coffeeId: it.coffee_table_id,
+ quantity: it.packs, // packs count
+ }))
+ : undefined,
+ }))
+ : []
+ if (active) setData(list)
+ } catch (e: any) {
+ if (active) setError(e?.message || 'Failed to load subscriptions.')
+ } finally {
+ if (active) setLoading(false)
+ }
+ })()
+ return () => { active = false }
+ }, [])
+
+ return { data, loading, error }
+}
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx
index 93226d8..fb3f602 100644
--- a/src/app/profile/page.tsx
+++ b/src/app/profile/page.tsx
@@ -10,6 +10,7 @@ import BasicInformation from './components/basicInformation'
import MediaSection from './components/mediaSection'
import BankInformation from './components/bankInformation'
import EditModal from './components/editModal'
+import UserAbo from './components/userAbo'
import {
UserCircleIcon,
EnvelopeIcon,
@@ -378,6 +379,8 @@ export default function ProfilePage() {
{/* Bank Info, Media */}
+ {/* --- My Abo Section (above bank info) --- */}
+
{/* --- Edit Bank Information Section --- */}