diff --git a/src/App.tsx b/src/App.tsx index 8524ea4..60ee5aa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { MusicPlayerProvider } from '@/contexts/MusicPlayerContext' import { SocketProvider } from '@/contexts/SocketContext' import { AccountPage } from '@/pages/AccountPage' import { AdminSoundsPage } from '@/pages/AdminSoundsPage' +import { AdminUsersPage } from '@/pages/AdminUsersPage' import { DashboardPage } from '@/pages/DashboardPage' import { LoginPage } from '@/pages/LoginPage' import { RegisterPage } from '@/pages/RegisterPage' @@ -78,6 +79,19 @@ function App() { } /> + + + + + + } + /> , + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } \ No newline at end of file diff --git a/src/pages/AdminUsersPage.tsx b/src/pages/AdminUsersPage.tsx new file mode 100644 index 0000000..7528ac5 --- /dev/null +++ b/src/pages/AdminUsersPage.tsx @@ -0,0 +1,415 @@ +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from '@/components/ui/sheet'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + RefreshCw, + Edit, + UserX, + UserCheck, + Users +} from 'lucide-react'; +import { toast } from 'sonner'; +import { apiService } from '@/services/api'; + +interface User { + id: string; + email: string; + name: string; + role: string; + is_active: boolean; + credits: number; + created_at: string; + providers: string[]; + plan: { + id: number; + code: string; + name: string; + description: string; + credits: number; + max_credits: number; + }; +} + +interface Plan { + id: number; + code: string; + name: string; + description: string; + credits: number; + max_credits: number; +} + +export function AdminUsersPage() { + const [users, setUsers] = useState([]); + const [plans, setPlans] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [editingUser, setEditingUser] = useState(null); + const [editForm, setEditForm] = useState({ + name: '', + credits: 0, + plan_id: 0, + is_active: true + }); + + useEffect(() => { + fetchUsers(); + fetchPlans(); + }, []); + + const fetchUsers = async () => { + try { + const response = await apiService.get('/api/admin/users'); + const data = await response.json(); + + if (response.ok) { + setUsers(data.users); + } else { + toast.error(data.error || 'Failed to fetch users'); + } + } catch { + toast.error('Failed to fetch users'); + } finally { + setLoading(false); + } + }; + + const fetchPlans = async () => { + try { + const response = await apiService.get('/api/admin/plans'); + const data = await response.json(); + + if (response.ok) { + setPlans(data.plans); + } else { + toast.error(data.error || 'Failed to fetch plans'); + } + } catch { + toast.error('Failed to fetch plans'); + } + }; + + const handleEditUser = (user: User) => { + setEditingUser(user); + setEditForm({ + name: user.name, + credits: user.credits, + plan_id: user.plan.id, + is_active: user.is_active + }); + }; + + const handleUpdateUser = async () => { + if (!editingUser) return; + + try { + const response = await apiService.patch(`/api/admin/users/${editingUser.id}`, editForm); + const data = await response.json(); + + if (response.ok) { + toast.success('User updated successfully'); + setEditingUser(null); + fetchUsers(); // Refresh users list + // Note: The sheet will close automatically when Save Changes button is wrapped with SheetClose + } else { + toast.error(data.error || 'Failed to update user'); + } + } catch { + toast.error('Failed to update user'); + } + }; + + const handleToggleUserStatus = async (user: User) => { + try { + const endpoint = user.is_active ? 'deactivate' : 'activate'; + const response = await apiService.post(`/api/admin/users/${user.id}/${endpoint}`); + const data = await response.json(); + + if (response.ok) { + const action = user.is_active ? 'deactivated' : 'activated'; + toast.success(`User ${action} successfully`); + fetchUsers(); // Refresh users list + } else { + toast.error(data.error || `Failed to ${endpoint} user`); + } + } catch { + toast.error('Failed to update user status'); + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + }; + + const getStatusBadge = (user: User) => { + return user.is_active ? ( + Active + ) : ( + Inactive + ); + }; + + const getRoleBadge = (role: string) => { + return role === 'admin' ? ( + Admin + ) : ( + User + ); + }; + + const filteredUsers = users.filter(user => + user.name.toLowerCase().includes(searchTerm.toLowerCase()) || + user.email.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+ +

Users Management

+
+ +
+ +
+
+ setSearchTerm(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-background" + /> +
+
+ {filteredUsers.length} of {users.length} users +
+
+ +
+ {filteredUsers.map((user) => ( + + +
+
+ {user.name} + {user.email} +
+
+ {getStatusBadge(user)} + {getRoleBadge(user.role)} +
+
+
+ +
+
+

User ID

+

{user.id}

+
+
+

Credits

+

{user.credits}

+
+
+

Plan

+

{user.plan.name}

+
+
+

Joined

+

{formatDate(user.created_at)}

+
+
+ +
+ + + + + +
+ + Edit User: {editingUser?.name} + + + {/* User Information Section */} +
+
+

User Information

+ +
+
+

User ID

+

{editingUser?.id}

+
+
+

Email

+

{editingUser?.email}

+
+
+

Role

+
+ {editingUser?.role === 'admin' ? ( + Admin + ) : ( + User + )} +
+
+
+

Joined

+

{editingUser ? formatDate(editingUser.created_at) : ''}

+
+
+ +
+

Authentication Providers

+
+ {editingUser?.providers?.map((provider: string) => ( + + {provider === 'password' ? 'Password' : + provider === 'api_token' ? 'API Token' : + provider === 'google' ? 'Google' : + provider === 'github' ? 'GitHub' : + provider.charAt(0).toUpperCase() + provider.slice(1)} + + )) || ( + No providers + )} +
+
+
+
+ + {/* Edit Form Section */} +
+
+

Edit Details

+ +
+
+ + setEditForm({...editForm, name: e.target.value})} + /> +
+
+ + setEditForm({...editForm, credits: parseInt(e.target.value) || 0})} + /> +
+
+ + +
+
+ setEditForm({...editForm, is_active: e.target.checked})} + className="w-4 h-4" + /> + +
+
+
+
+ + {/* Actions */} +
+ + + + + + +
+
+
+
+ + +
+
+
+ ))} +
+ + {filteredUsers.length === 0 && ( +
+ +

+ {searchTerm + ? 'No users found matching your search.' + : 'No users found'} +

+
+ )} +
+ ); +} \ No newline at end of file