diff --git a/src/App.tsx b/src/App.tsx index 3b8ad1b..e540610 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,6 +27,24 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { return <>{children} } +function AdminRoute({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth() + + if (loading) { + return
Loading...
+ } + + if (!user) { + return + } + + if (user.role !== 'admin') { + return + } + + return <>{children} +} + function AppRoutes() { const { user } = useAuth() @@ -56,14 +74,14 @@ function AppRoutes() { } /> + - + } /> + - + } /> ) diff --git a/src/lib/api/services/admin.ts b/src/lib/api/services/admin.ts new file mode 100644 index 0000000..5825a06 --- /dev/null +++ b/src/lib/api/services/admin.ts @@ -0,0 +1,54 @@ +import { apiClient } from '../client' +import type { User } from '@/types/auth' + +export interface Plan { + id: number + code: string + name: string + description?: string + credits: number + max_credits: number + created_at: string + updated_at: string +} + +export interface UserUpdate { + name?: string + plan_id?: number + credits?: number + is_active?: boolean +} + +export interface MessageResponse { + message: string +} + +export class AdminService { + async listUsers(limit = 100, offset = 0): Promise { + return apiClient.get(`/api/v1/admin/users/`, { + params: { limit, offset } + }) + } + + async getUser(userId: number): Promise { + return apiClient.get(`/api/v1/admin/users/${userId}`) + } + + async updateUser(userId: number, data: UserUpdate): Promise { + return apiClient.patch(`/api/v1/admin/users/${userId}`, data) + } + + async disableUser(userId: number): Promise { + return apiClient.post(`/api/v1/admin/users/${userId}/disable`) + } + + async enableUser(userId: number): Promise { + return apiClient.post(`/api/v1/admin/users/${userId}/enable`) + } + + async listPlans(): Promise { + return apiClient.get(`/api/v1/admin/users/plans/list`) + } +} + +export const adminService = new AdminService() \ No newline at end of file diff --git a/src/pages/admin/UsersPage.tsx b/src/pages/admin/UsersPage.tsx index 717ec0f..b99b28e 100644 --- a/src/pages/admin/UsersPage.tsx +++ b/src/pages/admin/UsersPage.tsx @@ -1,6 +1,153 @@ +import { useState, useEffect } from 'react' import { AppLayout } from '@/components/AppLayout' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { Badge } from '@/components/ui/badge' +import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Switch } from '@/components/ui/switch' +import { Skeleton } from '@/components/ui/skeleton' +import { toast } from 'sonner' +import { Edit, UserCheck, UserX } from 'lucide-react' +import { adminService, type Plan } from '@/lib/api/services/admin' +import type { User } from '@/types/auth' + +interface EditUserData { + name: string + plan_id: number + credits: number + is_active: boolean +} export function UsersPage() { + const [users, setUsers] = useState([]) + const [plans, setPlans] = useState([]) + const [loading, setLoading] = useState(true) + const [editingUser, setEditingUser] = useState(null) + const [editData, setEditData] = useState({ + name: '', + plan_id: 0, + credits: 0, + is_active: true + }) + const [saving, setSaving] = useState(false) + + useEffect(() => { + loadData() + }, []) + + const loadData = async () => { + try { + const [usersData, plansData] = await Promise.all([ + adminService.listUsers(), + adminService.listPlans() + ]) + setUsers(usersData) + setPlans(plansData) + } catch (error) { + toast.error('Failed to load data') + console.error('Error loading data:', error) + } finally { + setLoading(false) + } + } + + const handleEditUser = (user: User) => { + setEditingUser(user) + setEditData({ + name: user.name, + plan_id: user.plan.id, + credits: user.credits, + is_active: user.is_active + }) + } + + const handleSaveUser = async () => { + if (!editingUser) return + + setSaving(true) + try { + const updatedUser = await adminService.updateUser(editingUser.id, editData) + setUsers(prev => prev.map(u => u.id === editingUser.id ? updatedUser : u)) + setEditingUser(null) + toast.success('User updated successfully') + } catch (error) { + toast.error('Failed to update user') + console.error('Error updating user:', error) + } finally { + setSaving(false) + } + } + + const handleToggleUserStatus = async (user: User) => { + try { + if (user.is_active) { + await adminService.disableUser(user.id) + toast.success('User disabled successfully') + } else { + await adminService.enableUser(user.id) + toast.success('User enabled successfully') + } + // Reload data to get updated user status + loadData() + } catch (error) { + toast.error(`Failed to ${user.is_active ? 'disable' : 'enable'} user`) + console.error('Error toggling user status:', error) + } + } + + const getRoleBadge = (role: string) => { + return ( + + {role} + + ) + } + + const getStatusBadge = (isActive: boolean) => { + return ( + + {isActive ? 'Active' : 'Inactive'} + + ) + } + + if (loading) { + return ( + +
+
+ + +
+ + + + + +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+
+
+
+
+ ) + } + return ( -
-

User Management

-

- User administration interface coming soon... -

+
+
+

User Management

+ +
+ + + + Users ({users.length}) + + + + + + Name + Email + Role + Plan + Credits + Status + Actions + + + + {users.map((user) => ( + + {user.name} + {user.email} + {getRoleBadge(user.role)} + {user.plan.name} + {user.credits.toLocaleString()} + {getStatusBadge(user.is_active)} + +
+ + +
+
+
+ ))} +
+
+
+
+ + {/* Edit User Sheet */} + !open && setEditingUser(null)}> + +
+
+

Edit User

+
+ + {editingUser && ( +
+ {/* User Information Section */} +
+

User Information

+ +
+
+ User ID: + {editingUser.id} +
+
+ Email: + {editingUser.email} +
+
+ Role: + {getRoleBadge(editingUser.role)} +
+
+ Created: + {new Date(editingUser.created_at).toLocaleDateString()} +
+
+ Last Updated: + {new Date(editingUser.updated_at).toLocaleDateString()} +
+
+
+ + {/* Editable Fields Section */} +
+

Editable Settings

+ +
+
+ + setEditData(prev => ({ ...prev, name: e.target.value }))} + placeholder="Enter user's display name" + className="h-10" + /> +

+ This is the name displayed throughout the application +

+
+ +
+ + +

+ Current plan: {editingUser.plan.name} +

+
+ +
+ + setEditData(prev => ({ ...prev, credits: parseInt(e.target.value) || 0 }))} + placeholder="Enter credit amount" + className="h-10" + /> +

+ Maximum allowed: {editingUser.plan.max_credits.toLocaleString()} +

+
+ +
+ +
+
+ Allow Login Access + + {editData.is_active ? 'User can log in and use the platform' : 'User is blocked from logging in and accessing the platform'} + +
+ setEditData(prev => ({ ...prev, is_active: checked }))} + /> +
+
+
+
+ + {/* Action Buttons */} +
+ + +
+
+ )} +
+
+
) } \ No newline at end of file