diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 1424198..9e4985b 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -83,6 +83,12 @@ export default async function AdminPage() {

API Usage Monitoring & Statistics

+ + Manage Plans + +
+ {/* Header */} +
+
+ + + Back to Dashboard + +

Subscription Plans

+

Manage subscription tiers and monthly credits

+
+
+ + {/* Plan Management */} + +
+ + ); +} diff --git a/src/components/PlanManagementClient.tsx b/src/components/PlanManagementClient.tsx new file mode 100644 index 0000000..2214555 --- /dev/null +++ b/src/components/PlanManagementClient.tsx @@ -0,0 +1,321 @@ +'use client'; + +import { useState } from 'react'; +import { Plus, Edit, Trash2, X, Check, AlertCircle, Zap } from 'lucide-react'; +import { createPlan, updatePlan, deletePlan, grantMonthlyCredits, type SubscriptionPlan } from '@/services/subscription-service'; + +interface PlanManagementClientProps { + initialPlans: SubscriptionPlan[]; +} + +export default function PlanManagementClient({ initialPlans }: PlanManagementClientProps) { + const [plans, setPlans] = useState(initialPlans); + const [editingPlan, setEditingPlan] = useState(null); + const [isCreating, setIsCreating] = useState(false); + const [formData, setFormData] = useState({ + name: '', + display_name: '', + monthly_credits: 0, + price: 0, + description: '', + is_active: true, + sort_order: 0 + }); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + const handleEdit = (plan: SubscriptionPlan) => { + setEditingPlan(plan); + setFormData({ + name: plan.name, + display_name: plan.display_name, + monthly_credits: plan.monthly_credits, + price: plan.price, + description: plan.description || '', + is_active: plan.is_active, + sort_order: plan.sort_order + }); + setIsCreating(false); + setMessage(null); + }; + + const handleCreate = () => { + setEditingPlan(null); + setFormData({ + name: '', + display_name: '', + monthly_credits: 0, + price: 0, + description: '', + is_active: true, + sort_order: plans.length + 1 + }); + setIsCreating(true); + setMessage(null); + }; + + const handleSave = async () => { + if (!formData.name || !formData.display_name || formData.monthly_credits <= 0) { + setMessage({ type: 'error', text: 'Please fill in all required fields' }); + return; + } + + setLoading(true); + setMessage(null); + + if (isCreating) { + const result = await createPlan(formData); + if (result.success && result.plan) { + setPlans([...plans, result.plan]); + setMessage({ type: 'success', text: 'Plan created successfully' }); + setIsCreating(false); + } else { + setMessage({ type: 'error', text: result.error || 'Failed to create plan' }); + } + } else if (editingPlan) { + const result = await updatePlan(editingPlan.id, formData); + if (result.success) { + setPlans(plans.map(p => p.id === editingPlan.id ? { ...p, ...formData } : p)); + setMessage({ type: 'success', text: 'Plan updated successfully' }); + setEditingPlan(null); + } else { + setMessage({ type: 'error', text: result.error || 'Failed to update plan' }); + } + } + + setLoading(false); + }; + + const handleDelete = async (planId: string) => { + if (!confirm('Are you sure you want to delete this plan? Users on this plan will be unassigned.')) { + return; + } + + setLoading(true); + const result = await deletePlan(planId); + + if (result.success) { + setPlans(plans.filter(p => p.id !== planId)); + setMessage({ type: 'success', text: 'Plan deleted successfully' }); + } else { + setMessage({ type: 'error', text: result.error || 'Failed to delete plan' }); + } + + setLoading(false); + }; + + const handleGrantCredits = async () => { + if (!confirm('Grant monthly credits to all users based on their subscription plan?')) { + return; + } + + setLoading(true); + setMessage(null); + + const result = await grantMonthlyCredits(); + + if (result.success) { + setMessage({ + type: 'success', + text: `Credits granted! Processed: ${result.processed}, Failed: ${result.failed}` + }); + } else { + setMessage({ type: 'error', text: result.error || 'Failed to grant credits' }); + } + + setLoading(false); + }; + + return ( +
+ {/* Actions Bar */} +
+
+ + +
+
+ + {message && ( +
+ {message.type === 'success' ? : } + {message.text} +
+ )} + + {/* Plans Grid */} +
+ {plans.map((plan) => ( +
+ {!plan.is_active && ( +
+ Inactive +
+ )} +
+

{plan.display_name}

+

{plan.name}

+
+
+
{plan.monthly_credits}
+
Credits/Month
+
+
+
€{plan.price.toFixed(2)}
+
per month
+
+ {plan.description && ( +

{plan.description}

+ )} +
+ + +
+
+ ))} +
+ + {/* Edit/Create Modal */} + {(editingPlan || isCreating) && ( +
+
+
+

+ {isCreating ? 'Create Plan' : 'Edit Plan'} +

+ +
+ +
+
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="e.g. starter" + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> +
+
+ + setFormData({ ...formData, display_name: e.target.value })} + placeholder="e.g. Starter" + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> +
+
+ +
+
+ + setFormData({ ...formData, monthly_credits: parseInt(e.target.value) || 0 })} + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> +
+
+ + setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })} + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" + /> +
+
+ +
+ +