diff --git a/src/app/dashboard/subscription-plans/page.tsx b/src/app/dashboard/subscription-plans/page.tsx new file mode 100644 index 0000000..9b9726b --- /dev/null +++ b/src/app/dashboard/subscription-plans/page.tsx @@ -0,0 +1,164 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { adminStripeService, SubscriptionPlan, getErrorMessage } from '@/services'; + +export default function SubscriptionPlansPage() { + const router = useRouter(); + const [plans, setPlans] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [syncingId, setSyncingId] = useState(null); + const [syncMessage, setSyncMessage] = useState<{ planId: string; text: string; isError?: boolean } | null>(null); + + useEffect(() => { + fetchPlans(); + }, []); + + const fetchPlans = async () => { + setLoading(true); + setError(''); + try { + const data = await adminStripeService.getAdminPlans(); + setPlans(data); + } catch (err) { + const msg = getErrorMessage(err); + setError(msg); + if (msg.includes('Unauthorized')) router.push('/login'); + } finally { + setLoading(false); + } + }; + + const handleSync = async (plan: SubscriptionPlan) => { + setSyncingId(plan.id); + setSyncMessage(null); + try { + const updated = await adminStripeService.syncPlanFromStripe(plan.id); + setPlans((prev) => prev.map((p) => (p.id === updated.id ? updated : p))); + setSyncMessage({ + planId: plan.id, + text: `Synced: ${formatCurrency(updated.amount / 100)} / ${updated.interval}`, + }); + } catch (err) { + setSyncMessage({ planId: plan.id, text: getErrorMessage(err), isError: true }); + } finally { + setSyncingId(null); + setTimeout(() => setSyncMessage(null), 5000); + } + }; + + const formatCurrency = (amount: number) => + new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); + + return ( +
+
+

Subscription Plans

+

+ Manage subscription plans. Use "Sync from Stripe" to pull the latest amount, + currency, interval, name, and description directly from Stripe for a given price. +

+
+ +
+
+

All Plans ({plans.length})

+
+ + {error && ( +
+

{error}

+
+ )} + + {loading ? ( +
+
+
+ ) : plans.length === 0 ? ( +
No plans found
+ ) : ( +
+ + + + + + + + + + + + + {plans.map((plan) => ( + + + + + + + + + ))} + +
+ Name + + Stripe Price ID + + Amount + + Interval + + Active + + Actions +
+
{plan.name}
+ {plan.description && ( +
{plan.description}
+ )} +
+ + {plan.stripePriceId} + + + {formatCurrency(plan.amount / 100)} + {plan.currency.toUpperCase()} + + {plan.interval} + + + {plan.isActive ? 'Active' : 'Inactive'} + + + + {syncMessage && syncMessage.planId === plan.id && ( +

+ {syncMessage.text} +

+ )} +
+
+ )} +
+
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 4b5f7f2..f2b736a 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -92,6 +92,20 @@ const menuItems = [ ), }, + { + name: 'Subscription Plans', + href: '/dashboard/subscription-plans', + icon: ( + + + + ), + }, { name: 'Contact Messages', href: '/dashboard/contact-messages', diff --git a/src/services/stripe.service.ts b/src/services/stripe.service.ts index c27bda6..4b8ca5b 100644 --- a/src/services/stripe.service.ts +++ b/src/services/stripe.service.ts @@ -72,6 +72,18 @@ class AdminStripeService { return response.data.data; } + async getAdminPlans(): Promise { + const response = await api.get>('/stripe/admin/plans'); + return response.data.data; + } + + async syncPlanFromStripe(planId: string): Promise { + const response = await api.post>( + `/stripe/admin/plans/${planId}/sync`, + ); + return response.data.data; + } + async getRevenueStats(): Promise { const response = await api.get>('/stripe/admin/stats'); return response.data.data;