@@ -157,21 +127,12 @@ export function AppShell() {
-
-
- U
+ AD
diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts
new file mode 100644
index 0000000..90f0a80
--- /dev/null
+++ b/src/lib/api-client.ts
@@ -0,0 +1,248 @@
+import axios, { type AxiosInstance, type AxiosError } from 'axios';
+
+const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1';
+
+// Create axios instance
+const adminApi: AxiosInstance = axios.create({
+ baseURL: API_BASE_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Add token interceptor
+adminApi.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('access_token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+// Add response interceptor for error handling
+adminApi.interceptors.response.use(
+ (response) => response,
+ (error: AxiosError) => {
+ if (error.response?.status === 401) {
+ // Redirect to login
+ localStorage.removeItem('access_token');
+ window.location.href = '/login';
+ }
+ return Promise.reject(error);
+ }
+);
+
+// API helper functions
+export const adminApiHelpers = {
+ // Users
+ getUsers: (params?: {
+ page?: number;
+ limit?: number;
+ role?: string;
+ isActive?: boolean;
+ search?: string;
+ }) => adminApi.get('/admin/users', { params }),
+
+ getUser: (id: string) => adminApi.get(`/admin/users/${id}`),
+
+ getUserActivity: (id: string, days: number = 30) =>
+ adminApi.get(`/admin/users/${id}/activity`, { params: { days } }),
+
+ updateUser: (id: string, data: {
+ role?: string;
+ isActive?: boolean;
+ firstName?: string;
+ lastName?: string;
+ }) => adminApi.put(`/admin/users/${id}`, data),
+
+ deleteUser: (id: string, hard: boolean = false) =>
+ adminApi.delete(`/admin/users/${id}?hard=${hard}`),
+
+ resetPassword: (id: string) =>
+ adminApi.post(`/admin/users/${id}/reset-password`),
+
+ exportUsers: (format: string = 'csv') =>
+ adminApi.post('/admin/users/export', null, { params: { format } }),
+
+ importUsers: (file: File) => {
+ const formData = new FormData();
+ formData.append('file', file);
+ return adminApi.post('/admin/users/import', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+ },
+
+ // Logs
+ getLogs: (params?: {
+ page?: number;
+ limit?: number;
+ level?: string;
+ type?: string;
+ userId?: string;
+ startDate?: string;
+ endDate?: string;
+ search?: string;
+ minDuration?: number;
+ }) => adminApi.get('/admin/logs', { params }),
+
+ getErrorLogs: (params?: {
+ page?: number;
+ limit?: number;
+ userId?: string;
+ startDate?: string;
+ endDate?: string;
+ }) => adminApi.get('/admin/logs/errors', { params }),
+
+ getAccessLogs: (params?: {
+ page?: number;
+ limit?: number;
+ userId?: string;
+ startDate?: string;
+ endDate?: string;
+ }) => adminApi.get('/admin/logs/access', { params }),
+
+ getLogById: (id: string) => adminApi.get(`/admin/logs/${id}`),
+
+ getLogStats: (startDate?: string, endDate?: string) =>
+ adminApi.get('/admin/logs/stats/summary', { params: { startDate, endDate } }),
+
+ exportLogs: (params: {
+ format?: string;
+ level?: string;
+ startDate?: string;
+ endDate?: string;
+ }) => adminApi.post('/admin/logs/export', null, { params }),
+
+ cleanupLogs: (days: number = 30) =>
+ adminApi.post('/admin/logs/cleanup', null, { params: { days } }),
+
+ // Analytics
+ getOverview: () => adminApi.get('/admin/analytics/overview'),
+
+ getUserGrowth: (days: number = 30) =>
+ adminApi.get('/admin/analytics/users/growth', { params: { days } }),
+
+ getRevenue: (period: string = '30days') =>
+ adminApi.get('/admin/analytics/revenue', { params: { period } }),
+
+ getStorageAnalytics: () => adminApi.get('/admin/analytics/storage'),
+
+ getApiUsage: (days: number = 7) =>
+ adminApi.get('/admin/analytics/api-usage', { params: { days } }),
+
+ getErrorRate: (days: number = 7) =>
+ adminApi.get('/admin/analytics/error-rate', { params: { days } }),
+
+ // System
+ getHealth: () => adminApi.get('/admin/system/health'),
+
+ getSystemInfo: () => adminApi.get('/admin/system/info'),
+
+ getSettings: (category?: string) =>
+ adminApi.get('/admin/system/settings', { params: { category } }),
+
+ getSetting: (key: string) => adminApi.get(`/admin/system/settings/${key}`),
+
+ createSetting: (data: {
+ key: string;
+ value: string;
+ category: string;
+ description?: string;
+ isPublic?: boolean;
+ }) => adminApi.post('/admin/system/settings', data),
+
+ updateSetting: (key: string, data: {
+ value: string;
+ description?: string;
+ isPublic?: boolean;
+ }) => adminApi.put(`/admin/system/settings/${key}`, data),
+
+ deleteSetting: (key: string) => adminApi.delete(`/admin/system/settings/${key}`),
+
+ // Maintenance
+ getMaintenanceStatus: () => adminApi.get('/admin/maintenance'),
+
+ enableMaintenance: (message?: string) =>
+ adminApi.post('/admin/maintenance/enable', { message }),
+
+ disableMaintenance: () => adminApi.post('/admin/maintenance/disable'),
+
+ // Announcements
+ getAnnouncements: (activeOnly: boolean = false) =>
+ adminApi.get('/admin/announcements', { params: { activeOnly } }),
+
+ createAnnouncement: (data: {
+ title: string;
+ message: string;
+ type?: string;
+ priority?: number;
+ targetAudience?: string;
+ startsAt?: string;
+ endsAt?: string;
+ }) => adminApi.post('/admin/announcements', data),
+
+ updateAnnouncement: (id: string, data: {
+ title?: string;
+ message?: string;
+ type?: string;
+ priority?: number;
+ targetAudience?: string;
+ startsAt?: string;
+ endsAt?: string;
+ }) => adminApi.put(`/admin/announcements/${id}`, data),
+
+ toggleAnnouncement: (id: string) =>
+ adminApi.patch(`/admin/announcements/${id}/toggle`),
+
+ deleteAnnouncement: (id: string) =>
+ adminApi.delete(`/admin/announcements/${id}`),
+
+ // Audit
+ getAuditLogs: (params?: {
+ page?: number;
+ limit?: number;
+ userId?: string;
+ action?: string;
+ resourceType?: string;
+ resourceId?: string;
+ startDate?: string;
+ endDate?: string;
+ }) => adminApi.get('/admin/audit/logs', { params }),
+
+ getUserAuditActivity: (userId: string, days: number = 30) =>
+ adminApi.get(`/admin/audit/users/${userId}`, { params: { days } }),
+
+ getResourceHistory: (type: string, id: string) =>
+ adminApi.get(`/admin/audit/resource/${type}/${id}`),
+
+ getAuditStats: (startDate?: string, endDate?: string) =>
+ adminApi.get('/admin/audit/stats', { params: { startDate, endDate } }),
+
+ // Security
+ getFailedLogins: (params?: {
+ page?: number;
+ limit?: number;
+ email?: string;
+ ipAddress?: string;
+ }) => adminApi.get('/admin/security/failed-logins', { params }),
+
+ getSuspiciousActivity: () => adminApi.get('/admin/security/suspicious-activity'),
+
+ getAllApiKeys: () => adminApi.get('/admin/security/api-keys'),
+
+ revokeApiKey: (id: string) =>
+ adminApi.patch(`/admin/security/api-keys/${id}/revoke`),
+
+ getRateLimitViolations: (days: number = 7) =>
+ adminApi.get('/admin/security/rate-limits', { params: { days } }),
+
+ getActiveSessions: () => adminApi.get('/admin/security/sessions'),
+};
+
+export default adminApi;
+
diff --git a/src/main.tsx b/src/main.tsx
index f726fb0..450da0d 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -5,12 +5,14 @@ import App from './App.tsx'
import { BrowserRouter } from "react-router-dom"
import { QueryClientProvider } from "@tanstack/react-query"
import { queryClient } from "@/app/query-client"
+import { Toaster } from "@/components/ui/toast"
createRoot(document.getElementById('root')!).render(
+
,
diff --git a/src/pages/admin/analytics/api.tsx b/src/pages/admin/analytics/api.tsx
new file mode 100644
index 0000000..6aa9d6f
--- /dev/null
+++ b/src/pages/admin/analytics/api.tsx
@@ -0,0 +1,114 @@
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { adminApiHelpers } from "@/lib/api-client"
+
+export default function AnalyticsApiPage() {
+ const { data: apiUsage, isLoading } = useQuery({
+ queryKey: ['admin', 'analytics', 'api-usage'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getApiUsage(7)
+ return response.data
+ },
+ })
+
+ const { data: errorRate, isLoading: errorRateLoading } = useQuery({
+ queryKey: ['admin', 'analytics', 'error-rate'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getErrorRate(7)
+ return response.data
+ },
+ })
+
+ return (
+
+
API Usage Analytics
+
+
+
+
+ Total API Calls
+
+
+ {errorRateLoading ? (
+ ...
+ ) : (
+ {errorRate?.total || 0}
+ )}
+
+
+
+
+ Errors
+
+
+ {errorRateLoading ? (
+ ...
+ ) : (
+ {errorRate?.errors || 0}
+ )}
+
+
+
+
+ Error Rate
+
+
+ {errorRateLoading ? (
+ ...
+ ) : (
+
+ {errorRate?.errorRate ? `${errorRate.errorRate.toFixed(2)}%` : '0%'}
+
+ )}
+
+
+
+
+
+
+ Endpoint Usage (Last 7 Days)
+
+
+ {isLoading ? (
+ Loading API usage...
+ ) : (
+ <>
+
+
+
+ Endpoint
+ Calls
+ Avg Duration (ms)
+
+
+
+ {apiUsage?.map((endpoint: any, index: number) => (
+
+ {endpoint.endpoint}
+ {endpoint.calls}
+ {endpoint.avgDuration?.toFixed(2) || 'N/A'}
+
+ ))}
+
+
+ {apiUsage?.length === 0 && (
+
+ No API usage data available
+
+ )}
+ >
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/analytics/index.tsx b/src/pages/admin/analytics/index.tsx
new file mode 100644
index 0000000..8451611
--- /dev/null
+++ b/src/pages/admin/analytics/index.tsx
@@ -0,0 +1,87 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { BarChart3, Users, DollarSign, HardDrive, Activity } from "lucide-react"
+import { useNavigate } from "react-router-dom"
+
+export default function AnalyticsPage() {
+ const navigate = useNavigate()
+
+ return (
+
+
Analytics
+
+
+
navigate('/admin/analytics/overview')}>
+
+
+
+ Overview
+
+
+
+
+ Platform analytics overview
+
+
+
+
+
navigate('/admin/analytics/users')}>
+
+
+
+ Users Analytics
+
+
+
+
+ User growth and statistics
+
+
+
+
+
navigate('/admin/analytics/revenue')}>
+
+
+
+ Revenue Analytics
+
+
+
+
+ Revenue trends and breakdown
+
+
+
+
+
navigate('/admin/analytics/storage')}>
+
+
+
+ Storage Analytics
+
+
+
+
+ Storage usage and breakdown
+
+
+
+
+
navigate('/admin/analytics/api')}>
+
+
+
+ API Usage
+
+
+
+
+ API endpoint usage statistics
+
+
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/analytics/overview.tsx b/src/pages/admin/analytics/overview.tsx
new file mode 100644
index 0000000..273439e
--- /dev/null
+++ b/src/pages/admin/analytics/overview.tsx
@@ -0,0 +1,6 @@
+import DashboardPage from "../../dashboard"
+
+export default function AnalyticsOverviewPage() {
+ return
+}
+
diff --git a/src/pages/admin/analytics/revenue.tsx b/src/pages/admin/analytics/revenue.tsx
new file mode 100644
index 0000000..2fa11b1
--- /dev/null
+++ b/src/pages/admin/analytics/revenue.tsx
@@ -0,0 +1,47 @@
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
+import { adminApiHelpers } from "@/lib/api-client"
+
+export default function AnalyticsRevenuePage() {
+ const { data: revenue, isLoading } = useQuery({
+ queryKey: ['admin', 'analytics', 'revenue'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getRevenue('90days')
+ return response.data
+ },
+ })
+
+ return (
+
+
Revenue Analytics
+
+
+
+ Revenue Trends (Last 90 Days)
+
+
+ {isLoading ? (
+ Loading...
+ ) : revenue && revenue.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ No data available
+
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/analytics/storage.tsx b/src/pages/admin/analytics/storage.tsx
new file mode 100644
index 0000000..67a73d9
--- /dev/null
+++ b/src/pages/admin/analytics/storage.tsx
@@ -0,0 +1,119 @@
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts"
+import { adminApiHelpers } from "@/lib/api-client"
+
+const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']
+
+export default function AnalyticsStoragePage() {
+ const { data: storage, isLoading } = useQuery({
+ queryKey: ['admin', 'analytics', 'storage'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getStorageAnalytics()
+ return response.data
+ },
+ })
+
+ const formatBytes = (bytes: number) => {
+ if (bytes === 0) return '0 Bytes'
+ const k = 1024
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
+ }
+
+ const chartData = storage?.byCategory?.map((cat: any) => ({
+ name: cat.category,
+ value: cat.size,
+ })) || []
+
+ return (
+
+
Storage Analytics
+
+
+
+
+ Storage Overview
+
+
+ {isLoading ? (
+ Loading...
+ ) : (
+
+
+
Total Storage
+
+ {storage?.total ? formatBytes(storage.total.size) : '0 Bytes'}
+
+
+
+
Total Files
+
{storage?.total?.files || 0}
+
+
+ )}
+
+
+
+
+
+ Storage by Category
+
+
+ {isLoading ? (
+ Loading...
+ ) : chartData.length > 0 ? (
+
+
+ `${name} ${(percent * 100).toFixed(0)}%`}
+ outerRadius={80}
+ fill="#8884d8"
+ dataKey="value"
+ >
+ {chartData.map((entry: any, index: number) => (
+ |
+ ))}
+
+
+
+
+
+ ) : (
+
+ No data available
+
+ )}
+
+
+
+
+ {storage?.topUsers && storage.topUsers.length > 0 && (
+
+
+ Top 10 Users by Storage Usage
+
+
+
+ {storage.topUsers.map((user: any, index: number) => (
+
+
+
{user.user}
+
{user.files} files
+
+
{formatBytes(user.size)}
+
+ ))}
+
+
+
+ )}
+
+ )
+}
+
diff --git a/src/pages/admin/analytics/users.tsx b/src/pages/admin/analytics/users.tsx
new file mode 100644
index 0000000..9d4377a
--- /dev/null
+++ b/src/pages/admin/analytics/users.tsx
@@ -0,0 +1,49 @@
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
+import { adminApiHelpers } from "@/lib/api-client"
+
+export default function AnalyticsUsersPage() {
+ const { data: userGrowth, isLoading } = useQuery({
+ queryKey: ['admin', 'analytics', 'users', 'growth'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getUserGrowth(90)
+ return response.data
+ },
+ })
+
+ return (
+
+
User Analytics
+
+
+
+ User Growth (Last 90 Days)
+
+
+ {isLoading ? (
+ Loading...
+ ) : userGrowth && userGrowth.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ No data available
+
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/announcements/index.tsx b/src/pages/admin/announcements/index.tsx
new file mode 100644
index 0000000..8eea000
--- /dev/null
+++ b/src/pages/admin/announcements/index.tsx
@@ -0,0 +1,166 @@
+import { useState } from "react"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Megaphone, Plus, Edit, Trash2 } from "lucide-react"
+import { adminApiHelpers } from "@/lib/api-client"
+import { toast } from "sonner"
+import { format } from "date-fns"
+
+export default function AnnouncementsPage() {
+ const queryClient = useQueryClient()
+ const [createDialogOpen, setCreateDialogOpen] = useState(false)
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+ const [selectedAnnouncement, setSelectedAnnouncement] = useState
(null)
+
+ const { data: announcements, isLoading } = useQuery({
+ queryKey: ['admin', 'announcements'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getAnnouncements(false)
+ return response.data
+ },
+ })
+
+ const deleteMutation = useMutation({
+ mutationFn: async (id: string) => {
+ await adminApiHelpers.deleteAnnouncement(id)
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] })
+ toast.success("Announcement deleted successfully")
+ setDeleteDialogOpen(false)
+ },
+ onError: (error: any) => {
+ toast.error(error.response?.data?.message || "Failed to delete announcement")
+ },
+ })
+
+ const handleDelete = () => {
+ if (selectedAnnouncement) {
+ deleteMutation.mutate(selectedAnnouncement.id)
+ }
+ }
+
+ return (
+
+
+
Announcements
+
+
+
+
+
+ All Announcements
+
+
+ {isLoading ? (
+ Loading announcements...
+ ) : (
+ <>
+
+
+
+ Title
+ Type
+ Priority
+ Status
+ Start Date
+ End Date
+ Actions
+
+
+
+ {announcements?.map((announcement: any) => (
+
+ {announcement.title}
+
+ {announcement.type || 'info'}
+
+ {announcement.priority || 0}
+
+
+ {announcement.isActive ? 'Active' : 'Inactive'}
+
+
+
+ {announcement.startsAt ? format(new Date(announcement.startsAt), 'MMM dd, yyyy') : 'N/A'}
+
+
+ {announcement.endsAt ? format(new Date(announcement.endsAt), 'MMM dd, yyyy') : 'N/A'}
+
+
+
+
+
+
+
+
+ ))}
+
+
+ {announcements?.length === 0 && (
+
+ No announcements found
+
+ )}
+ >
+ )}
+
+
+
+ {/* Delete Dialog */}
+
+
+ )
+}
+
diff --git a/src/pages/admin/audit/index.tsx b/src/pages/admin/audit/index.tsx
new file mode 100644
index 0000000..6ccff8f
--- /dev/null
+++ b/src/pages/admin/audit/index.tsx
@@ -0,0 +1,106 @@
+import { useState } from "react"
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Badge } from "@/components/ui/badge"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { Search, Eye } from "lucide-react"
+import { adminApiHelpers } from "@/lib/api-client"
+import { format } from "date-fns"
+
+export default function AuditPage() {
+ const [page, setPage] = useState(1)
+ const [limit] = useState(50)
+ const [search, setSearch] = useState("")
+
+ const { data: auditData, isLoading } = useQuery({
+ queryKey: ['admin', 'audit', 'logs', page, limit, search],
+ queryFn: async () => {
+ const params: any = { page, limit }
+ if (search) params.search = search
+ const response = await adminApiHelpers.getAuditLogs(params)
+ return response.data
+ },
+ })
+
+ return (
+
+
Audit Logs
+
+
+
+
+
All Audit Logs
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+
+ {isLoading ? (
+ Loading audit logs...
+ ) : (
+ <>
+
+
+
+ Action
+ Resource Type
+ Resource ID
+ User
+ IP Address
+ Date
+ Actions
+
+
+
+ {auditData?.data?.map((log: any) => (
+
+
+ {log.action}
+
+ {log.resourceType}
+ {log.resourceId}
+ {log.userId || 'N/A'}
+ {log.ipAddress || 'N/A'}
+
+ {format(new Date(log.createdAt), 'MMM dd, yyyy HH:mm')}
+
+
+
+
+
+ ))}
+
+
+ {auditData?.data?.length === 0 && (
+
+ No audit logs found
+
+ )}
+ >
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/dashboard/index.tsx b/src/pages/admin/dashboard/index.tsx
new file mode 100644
index 0000000..c222cd5
--- /dev/null
+++ b/src/pages/admin/dashboard/index.tsx
@@ -0,0 +1,254 @@
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Download, Users, FileText, DollarSign, HardDrive, TrendingUp, AlertCircle } from "lucide-react"
+import { adminApiHelpers } from "@/lib/api-client"
+import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
+import { toast } from "sonner"
+
+export default function DashboardPage() {
+ const { data: overview, isLoading: overviewLoading } = useQuery({
+ queryKey: ['admin', 'analytics', 'overview'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getOverview()
+ return response.data
+ },
+ })
+
+ const { data: userGrowth, isLoading: growthLoading } = useQuery({
+ queryKey: ['admin', 'analytics', 'users', 'growth'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getUserGrowth(30)
+ return response.data
+ },
+ })
+
+ const { data: revenue, isLoading: revenueLoading } = useQuery({
+ queryKey: ['admin', 'analytics', 'revenue'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getRevenue('30days')
+ return response.data
+ },
+ })
+
+ const { data: health, isLoading: healthLoading } = useQuery({
+ queryKey: ['admin', 'system', 'health'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getHealth()
+ return response.data
+ },
+ })
+
+ const handleExport = () => {
+ toast.success("Exporting dashboard data...")
+ }
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(amount)
+ }
+
+ const formatBytes = (bytes: number) => {
+ if (bytes === 0) return '0 Bytes'
+ const k = 1024
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
+ }
+
+ return (
+
+
+
Dashboard Overview
+
+
+ {new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
+
+
+
+
+
+ {/* Stats Cards */}
+
+
+
+ Total Users
+
+
+
+ {overviewLoading ? (
+ ...
+ ) : (
+ <>
+ {overview?.users?.total || 0}
+
+ {overview?.users?.active || 0} active, {overview?.users?.inactive || 0} inactive
+
+ >
+ )}
+
+
+
+
+
+ Total Invoices
+
+
+
+ {overviewLoading ? (
+ ...
+ ) : (
+ <>
+ {overview?.invoices?.total || 0}
+
+ All time invoices
+
+ >
+ )}
+
+
+
+
+
+ Total Revenue
+
+
+
+ {overviewLoading ? (
+ ...
+ ) : (
+ <>
+
+ {overview?.revenue ? formatCurrency(overview.revenue.total) : '$0.00'}
+
+
+ Total revenue
+
+ >
+ )}
+
+
+
+
+
+ Storage Usage
+
+
+
+ {overviewLoading ? (
+ ...
+ ) : (
+ <>
+
+ {overview?.storage ? formatBytes(overview.storage.totalSize) : '0 Bytes'}
+
+
+ {overview?.storage?.documents || 0} documents
+
+ >
+ )}
+
+
+
+
+ {/* Charts Row */}
+
+ {/* User Growth Chart */}
+
+
+ User Growth (Last 30 Days)
+
+
+ {growthLoading ? (
+ Loading...
+ ) : userGrowth && userGrowth.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ No data available
+
+ )}
+
+
+
+ {/* Revenue Chart */}
+
+
+ Revenue Analytics (Last 30 Days)
+
+
+ {revenueLoading ? (
+ Loading...
+ ) : revenue && revenue.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ No data available
+
+ )}
+
+
+
+
+ {/* System Health */}
+
+
+
+
+ System Health
+
+
+
+ {healthLoading ? (
+ Loading system health...
+ ) : (
+
+
+
Status
+
{health?.status || 'Unknown'}
+
+
+
Database
+
{health?.database || 'Unknown'}
+
+
+
Recent Errors
+
{health?.recentErrors || 0}
+
+
+
Active Users
+
{health?.activeUsers || 0}
+
+
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/health/index.tsx b/src/pages/admin/health/index.tsx
new file mode 100644
index 0000000..a7a06fa
--- /dev/null
+++ b/src/pages/admin/health/index.tsx
@@ -0,0 +1,183 @@
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { AlertCircle, CheckCircle, XCircle, Database, Users, Activity } from "lucide-react"
+import { adminApiHelpers } from "@/lib/api-client"
+
+export default function HealthPage() {
+ const { data: health, isLoading: healthLoading } = useQuery({
+ queryKey: ['admin', 'system', 'health'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getHealth()
+ return response.data
+ },
+ refetchInterval: 30000, // Refetch every 30 seconds
+ })
+
+ const { data: systemInfo, isLoading: infoLoading } = useQuery({
+ queryKey: ['admin', 'system', 'info'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getSystemInfo()
+ return response.data
+ },
+ })
+
+ const getStatusIcon = (status: string) => {
+ switch (status?.toLowerCase()) {
+ case 'healthy':
+ case 'connected':
+ return
+ case 'degraded':
+ return
+ case 'disconnected':
+ case 'down':
+ return
+ default:
+ return
+ }
+ }
+
+ const formatUptime = (seconds: number) => {
+ const days = Math.floor(seconds / 86400)
+ const hours = Math.floor((seconds % 86400) / 3600)
+ const minutes = Math.floor((seconds % 3600) / 60)
+ return `${days}d ${hours}h ${minutes}m`
+ }
+
+ const formatBytes = (bytes: number) => {
+ if (bytes === 0) return '0 Bytes'
+ const k = 1024
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
+ }
+
+ return (
+
+
System Health
+
+
+
+
+
+ {getStatusIcon(health?.status)}
+ System Status
+
+
+
+ {healthLoading ? (
+ Loading...
+ ) : (
+
+ {health?.status || 'Unknown'}
+
+ )}
+
+
+
+
+
+
+ {getStatusIcon(health?.database)}
+ Database
+
+
+
+ {healthLoading ? (
+ Loading...
+ ) : (
+
+ {health?.database || 'Unknown'}
+
+ )}
+
+
+
+
+
+
+
+ Recent Errors
+
+
+
+ {healthLoading ? (
+ Loading...
+ ) : (
+ {health?.recentErrors || 0}
+ )}
+
+
+
+
+
+
+
+ Active Users
+
+
+
+ {healthLoading ? (
+ Loading...
+ ) : (
+ {health?.activeUsers || 0}
+ )}
+
+
+
+
+ {systemInfo && (
+
+
+ System Information
+
+
+ {infoLoading ? (
+ Loading system info...
+ ) : (
+
+
+
Node.js Version
+
{systemInfo.nodeVersion}
+
+
+
Platform
+
{systemInfo.platform}
+
+
+
Architecture
+
{systemInfo.architecture}
+
+
+
Uptime
+
{formatUptime(systemInfo.uptime)}
+
+
+
Environment
+
{systemInfo.env}
+
+
+
Memory Usage
+
+ {formatBytes(systemInfo.memory?.used || 0)} / {formatBytes(systemInfo.memory?.total || 0)}
+
+
+
+
CPU Cores
+
{systemInfo.cpu?.cores || 'N/A'}
+
+
+
Load Average
+
+ {systemInfo.cpu?.loadAverage?.map((load: number) => load.toFixed(2)).join(', ') || 'N/A'}
+
+
+
+ )}
+
+
+ )}
+
+ )
+}
+
diff --git a/src/pages/admin/maintenance/index.tsx b/src/pages/admin/maintenance/index.tsx
new file mode 100644
index 0000000..483437b
--- /dev/null
+++ b/src/pages/admin/maintenance/index.tsx
@@ -0,0 +1,116 @@
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Switch } from "@/components/ui/switch"
+import { Badge } from "@/components/ui/badge"
+import { adminApiHelpers } from "@/lib/api-client"
+import { toast } from "sonner"
+import { useState } from "react"
+
+export default function MaintenancePage() {
+ const queryClient = useQueryClient()
+ const [message, setMessage] = useState("")
+
+ const { data: status, isLoading } = useQuery({
+ queryKey: ['admin', 'maintenance'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getMaintenanceStatus()
+ return response.data
+ },
+ })
+
+ const enableMutation = useMutation({
+ mutationFn: async (msg?: string) => {
+ await adminApiHelpers.enableMaintenance(msg)
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
+ toast.success("Maintenance mode enabled")
+ setMessage("")
+ },
+ onError: (error: any) => {
+ toast.error(error.response?.data?.message || "Failed to enable maintenance mode")
+ },
+ })
+
+ const disableMutation = useMutation({
+ mutationFn: async () => {
+ await adminApiHelpers.disableMaintenance()
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['admin', 'maintenance'] })
+ toast.success("Maintenance mode disabled")
+ },
+ onError: (error: any) => {
+ toast.error(error.response?.data?.message || "Failed to disable maintenance mode")
+ },
+ })
+
+ const handleToggle = (enabled: boolean) => {
+ if (enabled) {
+ enableMutation.mutate(message || undefined)
+ } else {
+ disableMutation.mutate()
+ }
+ }
+
+ if (isLoading) {
+ return Loading maintenance status...
+ }
+
+ return (
+
+
Maintenance Mode
+
+
+
+
+ Maintenance Status
+
+ {status?.enabled ? 'Enabled' : 'Disabled'}
+
+
+
+
+
+
+
+
+ Enable maintenance mode to temporarily disable access to the platform
+
+
+
+
+
+ {!status?.enabled && (
+
+
+
setMessage(e.target.value)}
+ />
+
+ This message will be displayed to users when maintenance mode is enabled
+
+
+ )}
+
+ {status?.enabled && status?.message && (
+
+
+
{status.message}
+
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/security/api-keys.tsx b/src/pages/admin/security/api-keys.tsx
new file mode 100644
index 0000000..c2fcb9a
--- /dev/null
+++ b/src/pages/admin/security/api-keys.tsx
@@ -0,0 +1,105 @@
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { Key, Ban } from "lucide-react"
+import { adminApiHelpers } from "@/lib/api-client"
+import { toast } from "sonner"
+import { format } from "date-fns"
+
+export default function ApiKeysPage() {
+ const queryClient = useQueryClient()
+
+ const { data: apiKeys, isLoading } = useQuery({
+ queryKey: ['admin', 'security', 'api-keys'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getAllApiKeys()
+ return response.data
+ },
+ })
+
+ const revokeMutation = useMutation({
+ mutationFn: async (id: string) => {
+ await adminApiHelpers.revokeApiKey(id)
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['admin', 'security', 'api-keys'] })
+ toast.success("API key revoked successfully")
+ },
+ onError: (error: any) => {
+ toast.error(error.response?.data?.message || "Failed to revoke API key")
+ },
+ })
+
+ return (
+
+
API Keys
+
+
+
+ All API Keys
+
+
+ {isLoading ? (
+ Loading API keys...
+ ) : (
+ <>
+
+
+
+ Name
+ User
+ Last Used
+ Status
+ Actions
+
+
+
+ {apiKeys?.map((key: any) => (
+
+ {key.name}
+ {key.userId || 'N/A'}
+
+ {key.lastUsedAt ? format(new Date(key.lastUsedAt), 'MMM dd, yyyy') : 'Never'}
+
+
+
+ {key.revoked ? 'Revoked' : 'Active'}
+
+
+
+ {!key.revoked && (
+
+ )}
+
+
+ ))}
+
+
+ {apiKeys?.length === 0 && (
+
+ No API keys found
+
+ )}
+ >
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/security/failed-logins.tsx b/src/pages/admin/security/failed-logins.tsx
new file mode 100644
index 0000000..3a35a2f
--- /dev/null
+++ b/src/pages/admin/security/failed-logins.tsx
@@ -0,0 +1,105 @@
+import { useState } from "react"
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Input } from "@/components/ui/input"
+import { Badge } from "@/components/ui/badge"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { Search, Ban } from "lucide-react"
+import { adminApiHelpers } from "@/lib/api-client"
+import { format } from "date-fns"
+
+export default function FailedLoginsPage() {
+ const [page, setPage] = useState(1)
+ const [limit] = useState(50)
+ const [search, setSearch] = useState("")
+
+ const { data: failedLogins, isLoading } = useQuery({
+ queryKey: ['admin', 'security', 'failed-logins', page, limit, search],
+ queryFn: async () => {
+ const params: any = { page, limit }
+ if (search) params.email = search
+ const response = await adminApiHelpers.getFailedLogins(params)
+ return response.data
+ },
+ })
+
+ return (
+
+
Failed Login Attempts
+
+
+
+
+
Failed Logins
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+ {isLoading ? (
+ Loading failed logins...
+ ) : (
+ <>
+
+
+
+ Email
+ IP Address
+ User Agent
+ Reason
+ Attempted At
+ Blocked
+ Actions
+
+
+
+ {failedLogins?.data?.map((login: any) => (
+
+ {login.email}
+ {login.ipAddress}
+ {login.userAgent}
+ {login.reason || 'N/A'}
+
+ {format(new Date(login.attemptedAt), 'MMM dd, yyyy HH:mm')}
+
+
+
+ {login.blocked ? 'Yes' : 'No'}
+
+
+
+
+
+
+ ))}
+
+
+ {failedLogins?.data?.length === 0 && (
+
+ No failed login attempts found
+
+ )}
+ >
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/security/index.tsx b/src/pages/admin/security/index.tsx
new file mode 100644
index 0000000..78e1c0b
--- /dev/null
+++ b/src/pages/admin/security/index.tsx
@@ -0,0 +1,87 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Shield, AlertTriangle, Key, Gauge, Users } from "lucide-react"
+import { useNavigate } from "react-router-dom"
+
+export default function SecurityPage() {
+ const navigate = useNavigate()
+
+ return (
+
+
Security
+
+
+
navigate('/admin/security/failed-logins')}>
+
+
+
+ Failed Logins
+
+
+
+
+ View and manage failed login attempts
+
+
+
+
+
navigate('/admin/security/suspicious')}>
+
+
+
+ Suspicious Activity
+
+
+
+
+ Monitor suspicious IPs and emails
+
+
+
+
+
navigate('/admin/security/api-keys')}>
+
+
+
+ API Keys
+
+
+
+
+ Manage API keys and tokens
+
+
+
+
+
navigate('/admin/security/rate-limits')}>
+
+
+
+ Rate Limits
+
+
+
+
+ View rate limit violations
+
+
+
+
+
navigate('/admin/security/sessions')}>
+
+
+
+ Active Sessions
+
+
+
+
+ Manage active user sessions
+
+
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/security/rate-limits.tsx b/src/pages/admin/security/rate-limits.tsx
new file mode 100644
index 0000000..ab25f52
--- /dev/null
+++ b/src/pages/admin/security/rate-limits.tsx
@@ -0,0 +1,67 @@
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { adminApiHelpers } from "@/lib/api-client"
+
+export default function RateLimitsPage() {
+ const { data: violations, isLoading } = useQuery({
+ queryKey: ['admin', 'security', 'rate-limits'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getRateLimitViolations(7)
+ return response.data
+ },
+ })
+
+ return (
+
+
Rate Limit Violations
+
+
+
+ Recent Violations (Last 7 Days)
+
+
+ {isLoading ? (
+ Loading violations...
+ ) : (
+ <>
+
+
+
+ User
+ IP Address
+ Requests
+ Period
+
+
+
+ {violations?.map((violation: any) => (
+
+ {violation.userId || 'N/A'}
+ {violation.ipAddress}
+ {violation.requests}
+ {violation.period}
+
+ ))}
+
+
+ {violations?.length === 0 && (
+
+ No rate limit violations found
+
+ )}
+ >
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/security/sessions.tsx b/src/pages/admin/security/sessions.tsx
new file mode 100644
index 0000000..891b9a0
--- /dev/null
+++ b/src/pages/admin/security/sessions.tsx
@@ -0,0 +1,78 @@
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { LogOut } from "lucide-react"
+import { adminApiHelpers } from "@/lib/api-client"
+import { format } from "date-fns"
+
+export default function SessionsPage() {
+ const { data: sessions, isLoading } = useQuery({
+ queryKey: ['admin', 'security', 'sessions'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getActiveSessions()
+ return response.data
+ },
+ })
+
+ return (
+
+
Active Sessions
+
+
+
+ All Active Sessions
+
+
+ {isLoading ? (
+ Loading sessions...
+ ) : (
+ <>
+
+
+
+ User
+ IP Address
+ User Agent
+ Last Activity
+ Actions
+
+
+
+ {sessions?.map((session: any) => (
+
+ {session.userId || 'N/A'}
+ {session.ipAddress}
+ {session.userAgent}
+
+ {format(new Date(session.lastActivity), 'MMM dd, yyyy HH:mm')}
+
+
+
+
+
+ ))}
+
+
+ {sessions?.length === 0 && (
+
+ No active sessions found
+
+ )}
+ >
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/security/suspicious.tsx b/src/pages/admin/security/suspicious.tsx
new file mode 100644
index 0000000..34d7417
--- /dev/null
+++ b/src/pages/admin/security/suspicious.tsx
@@ -0,0 +1,91 @@
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Shield, Ban } from "lucide-react"
+import { adminApiHelpers } from "@/lib/api-client"
+
+export default function SuspiciousActivityPage() {
+ const { data: suspicious, isLoading } = useQuery({
+ queryKey: ['admin', 'security', 'suspicious'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getSuspiciousActivity()
+ return response.data
+ },
+ })
+
+ return (
+
+
Suspicious Activity
+
+
+
+
+
+
+ Suspicious IP Addresses
+
+
+
+ {isLoading ? (
+ Loading...
+ ) : suspicious?.suspiciousIPs?.length > 0 ? (
+
+ {suspicious.suspiciousIPs.map((ip: any, index: number) => (
+
+
+
{ip.ipAddress}
+
{ip.attempts} attempts
+
+
+
+ ))}
+
+ ) : (
+
+ No suspicious IPs found
+
+ )}
+
+
+
+
+
+
+
+ Suspicious Emails
+
+
+
+ {isLoading ? (
+ Loading...
+ ) : suspicious?.suspiciousEmails?.length > 0 ? (
+
+ {suspicious.suspiciousEmails.map((email: any, index: number) => (
+
+
+
{email.email}
+
{email.attempts} attempts
+
+
+
+ ))}
+
+ ) : (
+
+ No suspicious emails found
+
+ )}
+
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/settings/index.tsx b/src/pages/admin/settings/index.tsx
new file mode 100644
index 0000000..f805dd5
--- /dev/null
+++ b/src/pages/admin/settings/index.tsx
@@ -0,0 +1,94 @@
+import { useState } from "react"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { adminApiHelpers } from "@/lib/api-client"
+import { toast } from "sonner"
+
+export default function SettingsPage() {
+ const queryClient = useQueryClient()
+ const [selectedCategory, setSelectedCategory] = useState("GENERAL")
+
+ const { data: settings, isLoading } = useQuery({
+ queryKey: ['admin', 'settings', selectedCategory],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getSettings(selectedCategory)
+ return response.data
+ },
+ })
+
+ const updateSettingMutation = useMutation({
+ mutationFn: async ({ key, value }: { key: string; value: string }) => {
+ await adminApiHelpers.updateSetting(key, { value })
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
+ toast.success("Setting updated successfully")
+ },
+ onError: (error: any) => {
+ toast.error(error.response?.data?.message || "Failed to update setting")
+ },
+ })
+
+ const handleSave = (key: string, value: string) => {
+ updateSettingMutation.mutate({ key, value })
+ }
+
+ return (
+
+
System Settings
+
+
+
+ General
+ Email
+ Storage
+ Security
+ API
+ Features
+
+
+
+
+
+ {selectedCategory} Settings
+
+
+ {isLoading ? (
+ Loading settings...
+ ) : settings && settings.length > 0 ? (
+ settings.map((setting: any) => (
+
+
+
+ {
+ if (e.target.value !== setting.value) {
+ handleSave(setting.key, e.target.value)
+ }
+ }}
+ />
+
+ {setting.description && (
+
{setting.description}
+ )}
+
+ ))
+ ) : (
+
+ No settings found for this category
+
+ )}
+
+
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/users/[id]/activity.tsx b/src/pages/admin/users/[id]/activity.tsx
new file mode 100644
index 0000000..29f17b2
--- /dev/null
+++ b/src/pages/admin/users/[id]/activity.tsx
@@ -0,0 +1,66 @@
+import { useParams, useNavigate } from "react-router-dom"
+import { useQuery } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { ArrowLeft } from "lucide-react"
+import { adminApiHelpers } from "@/lib/api-client"
+import { format } from "date-fns"
+
+export default function UserActivityPage() {
+ const { id } = useParams()
+ const navigate = useNavigate()
+
+ const { data: activity, isLoading } = useQuery({
+ queryKey: ['admin', 'users', id, 'activity'],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getUserActivity(id!, 30)
+ return response.data
+ },
+ enabled: !!id,
+ })
+
+ if (isLoading) {
+ return Loading activity...
+ }
+
+ return (
+
+
+
+
User Activity
+
+
+
+
+ Activity Timeline (Last 30 Days)
+
+
+ {activity && activity.length > 0 ? (
+
+ {activity.map((item: any, index: number) => (
+
+
+
+
{item.action || 'Activity'}
+
{item.description || item.message}
+
+
+ {format(new Date(item.createdAt || item.timestamp), 'PPpp')}
+
+
+
+ ))}
+
+ ) : (
+
+ No activity found
+
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/users/[id]/index.tsx b/src/pages/admin/users/[id]/index.tsx
new file mode 100644
index 0000000..ce700b4
--- /dev/null
+++ b/src/pages/admin/users/[id]/index.tsx
@@ -0,0 +1,155 @@
+import { useParams, useNavigate } from "react-router-dom"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { ArrowLeft, Edit, Key, Trash2 } from "lucide-react"
+import { adminApiHelpers } from "@/lib/api-client"
+import { toast } from "sonner"
+import { format } from "date-fns"
+
+export default function UserDetailsPage() {
+ const { id } = useParams()
+ const navigate = useNavigate()
+ const queryClient = useQueryClient()
+
+ const { data: user, isLoading } = useQuery({
+ queryKey: ['admin', 'users', id],
+ queryFn: async () => {
+ const response = await adminApiHelpers.getUser(id!)
+ return response.data
+ },
+ enabled: !!id,
+ })
+
+ const updateUserMutation = useMutation({
+ mutationFn: async (data: any) => {
+ await adminApiHelpers.updateUser(id!, data)
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['admin', 'users', id] })
+ toast.success("User updated successfully")
+ },
+ onError: (error: any) => {
+ toast.error(error.response?.data?.message || "Failed to update user")
+ },
+ })
+
+ if (isLoading) {
+ return Loading user details...
+ }
+
+ if (!user) {
+ return User not found
+ }
+
+ return (
+
+
+
+
User Details
+
+
+
+
+ Information
+ Statistics
+ navigate(`/admin/users/${id}/activity`)}>
+ Activity
+
+
+
+
+
+
+
+
User Information
+
+
+
+
+
+
+
+
+
+
+
Name
+
{user.firstName} {user.lastName}
+
+
+
+
Status
+
+ {user.isActive ? 'Active' : 'Inactive'}
+
+
+
+
Created At
+
{format(new Date(user.createdAt), 'PPpp')}
+
+
+
Updated At
+
{format(new Date(user.updatedAt), 'PPpp')}
+
+
+
+
+
+
+
+
+
+
+ Invoices
+
+
+ {user._count?.invoices || 0}
+
+
+
+
+ Reports
+
+
+ {user._count?.reports || 0}
+
+
+
+
+ Documents
+
+
+ {user._count?.documents || 0}
+
+
+
+
+ Payments
+
+
+ {user._count?.payments || 0}
+
+
+
+
+
+
+ )
+}
+
diff --git a/src/pages/admin/users/index.tsx b/src/pages/admin/users/index.tsx
new file mode 100644
index 0000000..4268f65
--- /dev/null
+++ b/src/pages/admin/users/index.tsx
@@ -0,0 +1,326 @@
+import { useState } from "react"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
+import { useNavigate } from "react-router-dom"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Badge } from "@/components/ui/badge"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Search, Download, Eye, MoreVertical, UserPlus, Edit, Trash2, Key } from "lucide-react"
+import { adminApiHelpers } from "@/lib/api-client"
+import { toast } from "sonner"
+import { format } from "date-fns"
+
+export default function UsersPage() {
+ const navigate = useNavigate()
+ const queryClient = useQueryClient()
+ const [page, setPage] = useState(1)
+ const [limit] = useState(20)
+ const [search, setSearch] = useState("")
+ const [roleFilter, setRoleFilter] = useState("all")
+ const [statusFilter, setStatusFilter] = useState("all")
+ const [selectedUser, setSelectedUser] = useState(null)
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+ const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false)
+
+ const { data: usersData, isLoading } = useQuery({
+ queryKey: ['admin', 'users', page, limit, search, roleFilter, statusFilter],
+ queryFn: async () => {
+ const params: any = { page, limit }
+ if (search) params.search = search
+ if (roleFilter !== 'all') params.role = roleFilter
+ if (statusFilter !== 'all') params.isActive = statusFilter === 'active'
+ const response = await adminApiHelpers.getUsers(params)
+ return response.data
+ },
+ })
+
+ const deleteUserMutation = useMutation({
+ mutationFn: async ({ id, hard }: { id: string; hard: boolean }) => {
+ await adminApiHelpers.deleteUser(id, hard)
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
+ toast.success("User deleted successfully")
+ setDeleteDialogOpen(false)
+ },
+ onError: (error: any) => {
+ toast.error(error.response?.data?.message || "Failed to delete user")
+ },
+ })
+
+ const resetPasswordMutation = useMutation({
+ mutationFn: async (id: string) => {
+ const response = await adminApiHelpers.resetPassword(id)
+ return response.data
+ },
+ onSuccess: (data) => {
+ toast.success(`Password reset. Temporary password: ${data.temporaryPassword}`)
+ setResetPasswordDialogOpen(false)
+ },
+ onError: (error: any) => {
+ toast.error(error.response?.data?.message || "Failed to reset password")
+ },
+ })
+
+ const handleExport = async () => {
+ try {
+ const response = await adminApiHelpers.exportUsers('csv')
+ const blob = new Blob([response.data], { type: 'text/csv' })
+ const url = window.URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `users-${new Date().toISOString()}.csv`
+ a.click()
+ toast.success("Users exported successfully")
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || "Failed to export users")
+ }
+ }
+
+ const handleDelete = () => {
+ if (selectedUser) {
+ deleteUserMutation.mutate({ id: selectedUser.id, hard: false })
+ }
+ }
+
+ const handleResetPassword = () => {
+ if (selectedUser) {
+ resetPasswordMutation.mutate(selectedUser.id)
+ }
+ }
+
+ const getRoleBadgeVariant = (role: string) => {
+ switch (role) {
+ case 'ADMIN':
+ return 'destructive'
+ case 'USER':
+ return 'default'
+ case 'VIEWER':
+ return 'secondary'
+ default:
+ return 'outline'
+ }
+ }
+
+ return (
+
+
+
Users Management
+
+
+
+
+
+
+
All Users
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+ {isLoading ? (
+ Loading users...
+ ) : (
+ <>
+
+
+
+ Email
+ Name
+ Role
+ Status
+ Created At
+ Actions
+
+
+
+ {usersData?.data?.map((user: any) => (
+
+ {user.email}
+ {user.firstName} {user.lastName}
+
+
+ {user.role}
+
+
+
+
+ {user.isActive ? 'Active' : 'Inactive'}
+
+
+
+ {format(new Date(user.createdAt), 'MMM dd, yyyy')}
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ {usersData?.data?.length === 0 && (
+
+ No users found
+
+ )}
+ {usersData && usersData.total > limit && (
+
+
+ Showing {(page - 1) * limit + 1} to {Math.min(page * limit, usersData.total)} of {usersData.total} users
+
+
+
+
+
+
+ )}
+ >
+ )}
+
+
+
+ {/* Delete Dialog */}
+
+
+ {/* Reset Password Dialog */}
+
+
+ )
+}
+