feat: add plan assignment to user management
- Added plan dropdown to user edit modal - Shows current plan with highlighted card - Allows admin to assign/change user's subscription plan - Loads user's current plan when opening edit modal - Updates plan via setUserPlan service - Visual feedback with success/error messages Admins can now: - View user's current subscription plan - Assign users to different plans (Starter, Bronze, Silver, Gold) - See plan details (credits/month, price) in dropdown This completes the subscription plan system!
This commit is contained in:
@@ -3,6 +3,7 @@ import { cookies } from 'next/headers';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { checkIsAdmin } from '@/services/track-api-usage';
|
import { checkIsAdmin } from '@/services/track-api-usage';
|
||||||
import { getAllUsersWithCredits } from '@/services/admin-credit-service';
|
import { getAllUsersWithCredits } from '@/services/admin-credit-service';
|
||||||
|
import { getAllPlans } from '@/services/subscription-service';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ChevronLeft, Users, Coins, TrendingUp, TrendingDown } from 'lucide-react';
|
import { ChevronLeft, Users, Coins, TrendingUp, TrendingDown } from 'lucide-react';
|
||||||
import UserManagementClient from '@/components/UserManagementClient';
|
import UserManagementClient from '@/components/UserManagementClient';
|
||||||
@@ -23,6 +24,9 @@ export default async function AdminUsersPage() {
|
|||||||
// Fetch all users with credits
|
// Fetch all users with credits
|
||||||
const users = await getAllUsersWithCredits();
|
const users = await getAllUsersWithCredits();
|
||||||
|
|
||||||
|
// Fetch all plans
|
||||||
|
const plans = await getAllPlans();
|
||||||
|
|
||||||
// Calculate statistics
|
// Calculate statistics
|
||||||
const totalUsers = users.length;
|
const totalUsers = users.length;
|
||||||
const totalCreditsInCirculation = users.reduce((sum, u) => sum + u.balance, 0);
|
const totalCreditsInCirculation = users.reduce((sum, u) => sum + u.balance, 0);
|
||||||
@@ -91,7 +95,7 @@ export default async function AdminUsersPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User Management Table */}
|
{/* User Management Table */}
|
||||||
<UserManagementClient initialUsers={users} />
|
<UserManagementClient initialUsers={users} plans={plans} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Edit, Plus, Search, X, Check, AlertCircle } from 'lucide-react';
|
import { Edit, Plus, Search, X, Check, AlertCircle } from 'lucide-react';
|
||||||
import { updateUserCredits, setUserDailyLimit, setUserApiCosts } from '@/services/admin-credit-service';
|
import { updateUserCredits, setUserDailyLimit, setUserApiCosts } from '@/services/admin-credit-service';
|
||||||
|
import { setUserPlan, getUserSubscription, type SubscriptionPlan } from '@/services/subscription-service';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,9 +20,10 @@ interface User {
|
|||||||
|
|
||||||
interface UserManagementClientProps {
|
interface UserManagementClientProps {
|
||||||
initialUsers: User[];
|
initialUsers: User[];
|
||||||
|
plans: SubscriptionPlan[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserManagementClient({ initialUsers }: UserManagementClientProps) {
|
export default function UserManagementClient({ initialUsers, plans }: UserManagementClientProps) {
|
||||||
const [users, setUsers] = useState<User[]>(initialUsers);
|
const [users, setUsers] = useState<User[]>(initialUsers);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||||
@@ -30,6 +32,8 @@ export default function UserManagementClient({ initialUsers }: UserManagementCli
|
|||||||
const [dailyLimit, setDailyLimit] = useState('');
|
const [dailyLimit, setDailyLimit] = useState('');
|
||||||
const [googleCost, setGoogleCost] = useState('');
|
const [googleCost, setGoogleCost] = useState('');
|
||||||
const [geminiCost, setGeminiCost] = useState('');
|
const [geminiCost, setGeminiCost] = useState('');
|
||||||
|
const [selectedPlan, setSelectedPlan] = useState('');
|
||||||
|
const [currentPlan, setCurrentPlan] = useState<SubscriptionPlan | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||||
|
|
||||||
@@ -38,6 +42,16 @@ export default function UserManagementClient({ initialUsers }: UserManagementCli
|
|||||||
user.username.toLowerCase().includes(searchTerm.toLowerCase())
|
user.username.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Load user's current plan when editing
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingUser) {
|
||||||
|
getUserSubscription(editingUser.id).then(({ plan }) => {
|
||||||
|
setCurrentPlan(plan);
|
||||||
|
setSelectedPlan(plan?.id || '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [editingUser]);
|
||||||
|
|
||||||
const handleEditUser = (user: User) => {
|
const handleEditUser = (user: User) => {
|
||||||
setEditingUser(user);
|
setEditingUser(user);
|
||||||
setCreditAmount('');
|
setCreditAmount('');
|
||||||
@@ -48,6 +62,29 @@ export default function UserManagementClient({ initialUsers }: UserManagementCli
|
|||||||
setMessage(null);
|
setMessage(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdatePlan = async () => {
|
||||||
|
if (!editingUser || !selectedPlan) {
|
||||||
|
setMessage({ type: 'error', text: 'Please select a plan' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setMessage(null);
|
||||||
|
|
||||||
|
const result = await setUserPlan(editingUser.id, selectedPlan);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setMessage({ type: 'success', text: 'Plan updated successfully' });
|
||||||
|
// Reload current plan
|
||||||
|
const { plan } = await getUserSubscription(editingUser.id);
|
||||||
|
setCurrentPlan(plan);
|
||||||
|
} else {
|
||||||
|
setMessage({ type: 'error', text: result.error || 'Failed to update plan' });
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateCredits = async () => {
|
const handleUpdateCredits = async () => {
|
||||||
if (!editingUser || !creditAmount || !reason) {
|
if (!editingUser || !creditAmount || !reason) {
|
||||||
setMessage({ type: 'error', text: 'Please fill in all fields' });
|
setMessage({ type: 'error', text: 'Please fill in all fields' });
|
||||||
@@ -215,8 +252,8 @@ export default function UserManagementClient({ initialUsers }: UserManagementCli
|
|||||||
|
|
||||||
{message && (
|
{message && (
|
||||||
<div className={`mb-4 p-4 rounded-xl flex items-center gap-3 ${message.type === 'success'
|
<div className={`mb-4 p-4 rounded-xl flex items-center gap-3 ${message.type === 'success'
|
||||||
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-500'
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-500'
|
||||||
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-500'
|
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-500'
|
||||||
}`}>
|
}`}>
|
||||||
{message.type === 'success' ? <Check size={20} /> : <AlertCircle size={20} />}
|
{message.type === 'success' ? <Check size={20} /> : <AlertCircle size={20} />}
|
||||||
{message.text}
|
{message.text}
|
||||||
@@ -267,6 +304,43 @@ export default function UserManagementClient({ initialUsers }: UserManagementCli
|
|||||||
|
|
||||||
<hr className="border-zinc-200 dark:border-zinc-800" />
|
<hr className="border-zinc-200 dark:border-zinc-800" />
|
||||||
|
|
||||||
|
{/* Subscription Plan */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="font-bold text-zinc-900 dark:text-white">Subscription Plan</h4>
|
||||||
|
{currentPlan && (
|
||||||
|
<div className="p-4 bg-amber-50 dark:bg-amber-900/20 rounded-xl border border-amber-200 dark:border-amber-800">
|
||||||
|
<div className="text-xs font-bold text-amber-600 dark:text-amber-400 uppercase mb-1">Current Plan</div>
|
||||||
|
<div className="text-lg font-black text-amber-900 dark:text-amber-100">{currentPlan.display_name}</div>
|
||||||
|
<div className="text-sm text-amber-700 dark:text-amber-300 mt-1">{currentPlan.monthly_credits} credits/month</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-zinc-400 uppercase mb-2">Assign Plan</label>
|
||||||
|
<select
|
||||||
|
value={selectedPlan}
|
||||||
|
onChange={(e) => setSelectedPlan(e.target.value)}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<option value="">Select a plan...</option>
|
||||||
|
{plans.map(plan => (
|
||||||
|
<option key={plan.id} value={plan.id}>
|
||||||
|
{plan.display_name} - {plan.monthly_credits} credits/month (€{plan.price})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleUpdatePlan}
|
||||||
|
disabled={loading || !selectedPlan}
|
||||||
|
className="w-full py-3 bg-purple-600 hover:bg-purple-700 text-white font-bold rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Check size={18} />
|
||||||
|
Update Plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-zinc-200 dark:border-zinc-800" />
|
||||||
|
|
||||||
{/* Settings */}
|
{/* Settings */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h4 className="font-bold text-zinc-900 dark:text-white">User Settings</h4>
|
<h4 className="font-bold text-zinc-900 dark:text-white">User Settings</h4>
|
||||||
|
|||||||
Reference in New Issue
Block a user