'use client' import { useEffect, useState } from 'react' import { ADMIN_API_BASE } from '@/lib/api' // ─── Types ───────────────────────────────────────────────────────────────── interface PricingConfig { id: string plan: string billingPeriod: string amount: number updatedAt: string updatedBy: string | null } interface Promotion { id: string code: string name: string description: string | null discountType: 'PERCENTAGE' | 'FIXED' discountValue: number plans: string[] periods: string[] maxUses: number | null usedCount: number validFrom: string validUntil: string | null isActive: boolean createdAt: string } interface PlanFeature { id: string plan: string label: string sortOrder: number createdAt: string updatedAt: string } type PricingDraft = Record type PromoForm = { code: string name: string description: string discountType: 'PERCENTAGE' | 'FIXED' discountValue: string plans: string[] periods: string[] maxUses: string validFrom: string validUntil: string isActive: boolean } type PlanFeatureForm = { plan: string label: string sortOrder: string } // ─── Constants ───────────────────────────────────────────────────────────── const PLANS = ['STARTER', 'GROWTH', 'PRO'] const PERIODS = ['MONTHLY', 'ANNUAL'] const PLAN_COLORS: Record = { STARTER: 'text-zinc-400', GROWTH: 'text-sky-400', PRO: 'text-violet-400', } const PLAN_BADGE: Record = { STARTER: 'bg-zinc-800 text-zinc-300', GROWTH: 'bg-sky-900/40 text-sky-300', PRO: 'bg-violet-900/40 text-violet-300', } // ─── Helpers ─────────────────────────────────────────────────────────────── function pricingKey(plan: string, period: string) { return `${plan}:${period}` } function fmtCentimes(amount: number) { return (amount / 100).toFixed(2) } function parseCentimes(value: string): number | null { const n = parseFloat(value) if (isNaN(n) || n < 0) return null return Math.round(n * 100) } function fmtDate(iso: string | null) { if (!iso) return '—' return new Date(iso).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) } function toDateInputValue(iso: string | null) { if (!iso) return '' return iso.slice(0, 10) } function emptyPromoForm(): PromoForm { return { code: '', name: '', description: '', discountType: 'PERCENTAGE', discountValue: '', plans: [], periods: [], maxUses: '', validFrom: toDateInputValue(new Date().toISOString()), validUntil: '', isActive: true, } } function emptyPlanFeatureForm(plan = PLANS[0]): PlanFeatureForm { return { plan, label: '', sortOrder: '0', } } function authHeaders(): Record { return {} } function sortFeatures(list: PlanFeature[]) { return [...list].sort((a, b) => { const planDiff = PLANS.indexOf(a.plan) - PLANS.indexOf(b.plan) if (planDiff !== 0) return planDiff if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder return a.label.localeCompare(b.label) }) } // ─── Pricing table ────────────────────────────────────────────────────────── function PricingTable({ configs, draft, onChange, onSave, saving, error, successMsg, }: { configs: PricingConfig[] draft: PricingDraft onChange: (key: string, val: string) => void onSave: () => void saving: boolean error: string successMsg: string }) { const lastUpdated = configs .filter((c) => c.updatedAt) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())[0] return (

Plan prices

Enter values in MAD — stored as centimes internally.

{lastUpdated && (

Last updated {new Date(lastUpdated.updatedAt).toLocaleString()} {lastUpdated.updatedBy ? ` by ${lastUpdated.updatedBy}` : ''}

)}
{error &&
{error}
} {successMsg &&
{successMsg}
}
{PERIODS.map((p) => ( ))} {PLANS.map((plan, i) => ( {PERIODS.map((period) => { const k = pricingKey(plan, period) const config = configs.find((c) => c.plan === plan && c.billingPeriod === period) return ( ) })} ))}
Plan{p}
{plan}
onChange(k, e.target.value)} className="w-28 rounded-xl border border-stone-200 bg-white px-3 py-2 text-right text-sm font-medium text-blue-900 focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-[#07101e] dark:text-white" /> MAD
{config && (

Current: {fmtCentimes(config.amount)} MAD

)}
) } // ─── Plan features ────────────────────────────────────────────────────────── function PlanFeatureFormPanel({ initial, onSave, onCancel, saving, error, }: { initial: PlanFeatureForm onSave: (form: PlanFeatureForm) => void onCancel: () => void saving: boolean error: string }) { const [form, setForm] = useState(initial) useEffect(() => { setForm(initial) }, [initial]) const INPUT = 'w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-blue-900 focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-[#07101e] dark:text-white' const LABEL = 'mb-1.5 block text-xs font-semibold uppercase tracking-[0.14em] text-stone-500 dark:text-slate-400' return (
{error &&
{error}
}
) } function PlanFeaturesSection() { const [features, setFeatures] = useState([]) const [loading, setLoading] = useState(true) const [formState, setFormState] = useState<'hidden' | 'create' | string>('hidden') const [formInitial, setFormInitial] = useState(emptyPlanFeatureForm()) const [saving, setSaving] = useState(false) const [formError, setFormError] = useState('') const [deleteConfirm, setDeleteConfirm] = useState(null) useEffect(() => { fetch(`${ADMIN_API_BASE}/admin/pricing/features`, { headers: authHeaders(), credentials: 'include' }) .then(async (r) => { const json = await r.json() if (!r.ok) throw new Error(json?.message ?? 'Failed to load plan features') setFeatures(sortFeatures(json.data ?? [])) }) .finally(() => setLoading(false)) }, []) function openCreate(plan = PLANS[0]) { const nextOrder = Math.max( 0, ...features.filter((feature) => feature.plan === plan).map((feature) => feature.sortOrder), ) + 10 setFormInitial({ plan, label: '', sortOrder: String(nextOrder) }) setFormError('') setFormState('create') } function openEdit(feature: PlanFeature) { setFormInitial({ plan: feature.plan, label: feature.label, sortOrder: String(feature.sortOrder), }) setFormError('') setFormState(feature.id) } function buildPayload(form: PlanFeatureForm) { const label = form.label.trim() if (!label) return { error: 'Feature label is required' } const sortOrder = form.sortOrder.trim() === '' ? 0 : Number.parseInt(form.sortOrder, 10) if (Number.isNaN(sortOrder) || sortOrder < 0) return { error: 'Display order must be 0 or greater' } return { payload: { plan: form.plan, label, sortOrder, }, } } async function handleSave(form: PlanFeatureForm) { setFormError('') const result = buildPayload(form) if ('error' in result) { setFormError(result.error ?? 'Invalid plan feature') return } setSaving(true) try { const isEdit = formState !== 'create' const url = isEdit ? `${ADMIN_API_BASE}/admin/pricing/features/${formState}` : `${ADMIN_API_BASE}/admin/pricing/features` const res = await fetch(url, { method: isEdit ? 'PATCH' : 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() }, credentials: 'include', body: JSON.stringify(result.payload), }) const json = await res.json() if (!res.ok) throw new Error(json?.message ?? 'Save failed') const saved: PlanFeature = json.data setFeatures((prev) => sortFeatures( isEdit ? prev.map((feature) => (feature.id === saved.id ? saved : feature)) : [...prev, saved], )) setFormState('hidden') } catch (err) { setFormError(err instanceof Error ? err.message : 'Save failed') } finally { setSaving(false) } } async function handleDelete(id: string) { try { const res = await fetch(`${ADMIN_API_BASE}/admin/pricing/features/${id}`, { method: 'DELETE', headers: authHeaders(), credentials: 'include', }) if (!res.ok) throw new Error('Delete failed') setFeatures((prev) => prev.filter((feature) => feature.id !== id)) } catch { /* silent */ } finally { setDeleteConfirm(null) } } return (

Plan features

Control the feature bullets shown on each public pricing card.

{formState === 'hidden' && ( )}
{formState !== 'hidden' && ( setFormState('hidden')} saving={saving} error={formError} /> )} {loading ? (
) : features.length === 0 ? (

No plan features yet. Create one above.

) : (
{PLANS.map((plan) => { const planFeatures = features.filter((feature) => feature.plan === plan) return (

{plan}

{planFeatures.length} feature{planFeatures.length === 1 ? '' : 's'}

{formState === 'hidden' && ( )}
{planFeatures.length === 0 ? (
No features for this plan.
) : planFeatures.map((feature) => (

{feature.label}

Order {feature.sortOrder}

{deleteConfirm === feature.id ? ( <> ) : ( )}
))}
) })}
)}
) } // ─── Promotion form ───────────────────────────────────────────────────────── function PromotionFormPanel({ initial, onSave, onCancel, saving, error, }: { initial: PromoForm onSave: (form: PromoForm) => void onCancel: () => void saving: boolean error: string }) { const [form, setForm] = useState(initial) useEffect(() => { setForm(initial) }, [initial]) function set(k: K, v: PromoForm[K]) { setForm((f) => ({ ...f, [k]: v })) } function toggleArr(field: 'plans' | 'periods', val: string) { setForm((f) => { const arr = f[field] return { ...f, [field]: arr.includes(val) ? arr.filter((x) => x !== val) : [...arr, val] } }) } const INPUT = 'w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-blue-900 focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-[#07101e] dark:text-white' const LABEL = 'mb-1.5 block text-xs font-semibold uppercase tracking-[0.14em] text-stone-500 dark:text-slate-400' return (
{error &&
{error}
}
Discount type *
{(['PERCENTAGE', 'FIXED'] as const).map((t) => ( ))}
Applicable plans

Leave empty to apply to all plans

{PLANS.map((p) => ( ))}
Applicable periods

Leave empty to apply to all periods

{PERIODS.map((p) => ( ))}
Active
) } // ─── Promotions section ───────────────────────────────────────────────────── function PromotionsSection() { const [promotions, setPromotions] = useState([]) const [loading, setLoading] = useState(true) const [formState, setFormState] = useState<'hidden' | 'create' | string>('hidden') // string = editing id const [formInitial, setFormInitial] = useState(emptyPromoForm()) const [saving, setSaving] = useState(false) const [formError, setFormError] = useState('') const [deleteConfirm, setDeleteConfirm] = useState(null) useEffect(() => { fetch(`${ADMIN_API_BASE}/admin/pricing/promotions`, { headers: authHeaders(), credentials: 'include' }) .then(async (r) => { const json = await r.json() if (!r.ok) throw new Error(json?.message ?? 'Failed to load promotions') setPromotions(json.data ?? []) }) .finally(() => setLoading(false)) }, []) function openCreate() { setFormInitial(emptyPromoForm()) setFormError('') setFormState('create') } function openEdit(promo: Promotion) { setFormInitial({ code: promo.code, name: promo.name, description: promo.description ?? '', discountType: promo.discountType, discountValue: promo.discountType === 'FIXED' ? fmtCentimes(promo.discountValue) : String(promo.discountValue), plans: promo.plans, periods: promo.periods, maxUses: promo.maxUses != null ? String(promo.maxUses) : '', validFrom: toDateInputValue(promo.validFrom), validUntil: toDateInputValue(promo.validUntil), isActive: promo.isActive, }) setFormError('') setFormState(promo.id) } function buildPayload(form: PromoForm) { if (!form.code) return { error: 'Code is required' } if (!form.name) return { error: 'Name is required' } if (!form.discountValue) return { error: 'Discount value is required' } if (!form.validFrom) return { error: 'Valid from date is required' } const rawVal = parseFloat(form.discountValue) if (isNaN(rawVal) || rawVal <= 0) return { error: 'Discount value must be a positive number' } if (form.discountType === 'PERCENTAGE' && rawVal > 100) return { error: 'Percentage cannot exceed 100' } const discountValue = form.discountType === 'FIXED' ? Math.round(rawVal * 100) : Math.round(rawVal) return { payload: { code: form.code, name: form.name, description: form.description || undefined, discountType: form.discountType, discountValue, plans: form.plans, periods: form.periods, maxUses: form.maxUses ? parseInt(form.maxUses, 10) : null, validFrom: new Date(form.validFrom).toISOString(), validUntil: form.validUntil ? new Date(form.validUntil).toISOString() : null, isActive: form.isActive, }, } } async function handleSave(form: PromoForm) { setFormError('') const result = buildPayload(form) if ('error' in result) { setFormError(result.error ?? 'Invalid promotion form') return } setSaving(true) try { const isEdit = formState !== 'create' const url = isEdit ? `${ADMIN_API_BASE}/admin/pricing/promotions/${formState}` : `${ADMIN_API_BASE}/admin/pricing/promotions` const res = await fetch(url, { method: isEdit ? 'PATCH' : 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() }, credentials: 'include', body: JSON.stringify(result.payload), }) const json = await res.json() if (!res.ok) throw new Error(json?.message ?? 'Save failed') const saved: Promotion = json.data setPromotions((prev) => isEdit ? prev.map((p) => (p.id === saved.id ? saved : p)) : [saved, ...prev], ) setFormState('hidden') } catch (err) { setFormError(err instanceof Error ? err.message : 'Save failed') } finally { setSaving(false) } } async function handleToggleActive(promo: Promotion) { try { const res = await fetch(`${ADMIN_API_BASE}/admin/pricing/promotions/${promo.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', ...authHeaders() }, credentials: 'include', body: JSON.stringify({ isActive: !promo.isActive }), }) const json = await res.json() if (!res.ok) throw new Error(json?.message ?? 'Failed') setPromotions((prev) => prev.map((p) => (p.id === promo.id ? json.data : p))) } catch { /* silent */ } } async function handleDelete(id: string) { try { const res = await fetch(`${ADMIN_API_BASE}/admin/pricing/promotions/${id}`, { method: 'DELETE', headers: authHeaders(), credentials: 'include', }) if (!res.ok) throw new Error('Delete failed') setPromotions((prev) => prev.filter((p) => p.id !== id)) } catch { /* silent */ } finally { setDeleteConfirm(null) } } return (

Promotions & discounts

Promo codes applied during checkout to reduce subscription prices.

{formState === 'hidden' && ( )}
{formState !== 'hidden' && ( setFormState('hidden')} saving={saving} error={formError} /> )} {loading ? (
) : promotions.length === 0 ? (

No promotions yet. Create one above.

) : (
{['Code', 'Name', 'Discount', 'Plans', 'Periods', 'Validity', 'Uses', 'Status', ''].map((h) => ( ))} {promotions.map((promo, i) => { const isExpired = promo.validUntil && new Date(promo.validUntil) < new Date() const isEditing = formState === promo.id return ( ) })}
{h}
{promo.code}

{promo.name}

{promo.description &&

{promo.description}

}
{promo.discountType === 'PERCENTAGE' ? `${promo.discountValue}%` : `${fmtCentimes(promo.discountValue)} MAD`} {promo.plans.length === 0 ? ( All ) : (
{promo.plans.map((p) => ( {p} ))}
)}
{promo.periods.length === 0 ? ( All ) : (
{promo.periods.map((p) => ( {p} ))}
)}

{fmtDate(promo.validFrom)}

{promo.validUntil ? `→ ${fmtDate(promo.validUntil)}` : '→ No expiry'}

{promo.usedCount}{promo.maxUses != null ? ` / ${promo.maxUses}` : ''}
{deleteConfirm === promo.id ? ( <> ) : ( )}
)}
) } // ─── Page ─────────────────────────────────────────────────────────────────── export default function PricingPage() { const [configs, setConfigs] = useState([]) const [draft, setDraft] = useState({}) const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading') const [saving, setSaving] = useState(false) const [error, setError] = useState('') const [successMsg, setSuccessMsg] = useState('') useEffect(() => { fetch(`${ADMIN_API_BASE}/admin/pricing`, { headers: authHeaders(), credentials: 'include', cache: 'no-store', }) .then((r) => r.json()) .then((json) => { if (!json?.data) throw new Error(json?.message ?? 'Failed to load') const list: PricingConfig[] = json.data setConfigs(list) const initial: PricingDraft = {} for (const c of list) initial[pricingKey(c.plan, c.billingPeriod)] = fmtCentimes(c.amount) setDraft(initial) setStatus('ready') }) .catch((err) => { setError(err.message); setStatus('error') }) }, []) async function handleSavePrices() { setError('') setSuccessMsg('') const entries: { plan: string; billingPeriod: string; amount: number }[] = [] for (const plan of PLANS) { for (const period of PERIODS) { const k = pricingKey(plan, period) const amount = parseCentimes(draft[k] ?? '') if (amount === null) { setError(`Invalid value for ${plan} ${period}`); return } entries.push({ plan, billingPeriod: period, amount }) } } setSaving(true) try { const res = await fetch(`${ADMIN_API_BASE}/admin/pricing`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', ...authHeaders() }, credentials: 'include', body: JSON.stringify({ entries }), }) const json = await res.json() if (!res.ok) throw new Error(json?.message ?? 'Save failed') const updated: PricingConfig[] = json.data setConfigs(updated) const refreshed: PricingDraft = {} for (const c of updated) refreshed[pricingKey(c.plan, c.billingPeriod)] = fmtCentimes(c.amount) setDraft(refreshed) setSuccessMsg('Prices saved successfully.') } catch (err) { setError(err instanceof Error ? err.message : 'Save failed') } finally { setSaving(false) } } if (status === 'loading') { return (
) } if (status === 'error') { return (
{error}
) } return (

Platform

Subscription Pricing

Manage plan prices, plan features, and promotional discount codes.

setDraft((prev) => ({ ...prev, [k]: v }))} onSave={handleSavePrices} saving={saving} error={error} successMsg={successMsg} />
) }