diff --git a/src/App.tsx b/src/App.tsx index 7bd9aeb..1164c6e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,30 +1,35 @@ -import { Navigate, Route, Routes } from "react-router-dom" -import { AppShell } from "@/layouts/app-shell" -import { ProtectedRoute } from "@/components/ProtectedRoute" -import LoginPage from "@/pages/login" -import DashboardPage from "@/pages/admin/dashboard" -import UsersPage from "@/pages/admin/users" -import UserDetailsPage from "@/pages/admin/users/[id]" -import UserActivityPage from "@/pages/admin/users/[id]/activity" -import ActivityLogPage from "@/pages/activity-log" -import SettingsPage from "@/pages/admin/settings" -import MaintenancePage from "@/pages/admin/maintenance" -import AnnouncementsPage from "@/pages/admin/announcements" -import AuditPage from "@/pages/admin/audit" -import SecurityPage from "@/pages/admin/security" -import FailedLoginsPage from "@/pages/admin/security/failed-logins" -import SuspiciousActivityPage from "@/pages/admin/security/suspicious" -import ApiKeysPage from "@/pages/admin/security/api-keys" -import RateLimitsPage from "@/pages/admin/security/rate-limits" -import SessionsPage from "@/pages/admin/security/sessions" -import AnalyticsPage from "@/pages/admin/analytics" -import AnalyticsOverviewPage from "@/pages/admin/analytics/overview" -import AnalyticsUsersPage from "@/pages/admin/analytics/users" -import AnalyticsRevenuePage from "@/pages/admin/analytics/revenue" -import AnalyticsStoragePage from "@/pages/admin/analytics/storage" -import AnalyticsApiPage from "@/pages/admin/analytics/api" -import HealthPage from "@/pages/admin/health" -import NotificationsPage from "@/pages/notifications" +import { Navigate, Route, Routes } from "react-router-dom"; +import { AppShell } from "@/layouts/app-shell"; +import { ProtectedRoute } from "@/components/ProtectedRoute"; +import LoginPage from "@/pages/login"; +import DashboardPage from "@/pages/admin/dashboard"; +import UsersPage from "@/pages/admin/users"; +import UserDetailsPage from "@/pages/admin/users/[id]"; +import UserActivityPage from "@/pages/admin/users/[id]/activity"; +import ActivityLogPage from "@/pages/activity-log"; +import SettingsPage from "@/pages/admin/settings"; +import MaintenancePage from "@/pages/admin/maintenance"; +import AnnouncementsPage from "@/pages/admin/announcements"; +import AuditPage from "@/pages/admin/audit"; +import SecurityPage from "@/pages/admin/security"; +import FailedLoginsPage from "@/pages/admin/security/failed-logins"; +import SuspiciousActivityPage from "@/pages/admin/security/suspicious"; +import ApiKeysPage from "@/pages/admin/security/api-keys"; +import RateLimitsPage from "@/pages/admin/security/rate-limits"; +import SessionsPage from "@/pages/admin/security/sessions"; +import AnalyticsPage from "@/pages/admin/analytics"; +import AnalyticsOverviewPage from "@/pages/admin/analytics/overview"; +import AnalyticsUsersPage from "@/pages/admin/analytics/users"; +import AnalyticsRevenuePage from "@/pages/admin/analytics/revenue"; +import AnalyticsStoragePage from "@/pages/admin/analytics/storage"; +import AnalyticsApiPage from "@/pages/admin/analytics/api"; +import HealthPage from "@/pages/admin/health"; +import NotificationsPage from "@/pages/notifications"; +import PaymentsListPage from "@/pages/admin/payments/payments-list"; +import PaymentRequestsPage from "@/pages/admin/payments/payment-requests"; +import InvoicesPage from "@/pages/admin/invoices/invoices-list"; +import ProformaPage from "@/pages/admin/invoices/proforma-list"; +import ProformaRequestsPage from "@/pages/admin/invoices/proforma-requests"; function App() { return ( @@ -51,23 +56,49 @@ function App() { } /> } /> } /> - } /> - } /> + } + /> + } + /> } /> } /> } /> } /> - } /> + } + /> } /> - } /> - } /> + } + /> + } + /> } /> + } /> + } + /> + } /> + } /> + } + /> } /> } /> } /> - ) + ); } -export default App +export default App; diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 6aa2d85..3d688ee 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,17 +1,17 @@ -import { Navigate, useLocation } from "react-router-dom" +import { Navigate, useLocation } from "react-router-dom"; interface ProtectedRouteProps { - children: React.ReactNode + children: React.ReactNode; } export function ProtectedRoute({ children }: ProtectedRouteProps) { - const location = useLocation() - const token = localStorage.getItem('access_token') + // const location = useLocation() + // const token = localStorage.getItem('access_token') - if (!token) { - // Redirect to login page with return URL - return - } + // if (!token) { + // // Redirect to login page with return URL + // return + // } - return <>{children} + return <>{children}; } diff --git a/src/layouts/app-shell.tsx b/src/layouts/app-shell.tsx index e264b7f..019c9b4 100644 --- a/src/layouts/app-shell.tsx +++ b/src/layouts/app-shell.tsx @@ -1,5 +1,5 @@ -import { Outlet, Link, useLocation, useNavigate } from "react-router-dom" -import { useState } from "react" +import { Outlet, Link, useLocation, useNavigate } from "react-router-dom"; +import { useState } from "react"; import { LayoutDashboard, Users, @@ -14,10 +14,15 @@ import { Search, Bell, LogOut, -} from "lucide-react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Avatar, AvatarFallback } from "@/components/ui/avatar" + CreditCard, + FileClock, + Receipt, + FileSearch, + ClipboardList, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, @@ -25,20 +30,33 @@ import { DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { cn } from "@/lib/utils" -import { authService } from "@/services" -import { toast } from "sonner" +} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/utils"; +import { authService } from "@/services"; +import { toast } from "sonner"; interface User { - email: string - firstName?: string - lastName?: string - role: string + email: string; + firstName?: string; + lastName?: string; + role: string; } const adminNavigationItems = [ { icon: LayoutDashboard, label: "Dashboard", path: "/admin/dashboard" }, + { icon: Receipt, label: "Invoices", path: "/admin/invoices" }, + { icon: FileSearch, label: "Proforma", path: "/admin/proforma" }, + { + icon: ClipboardList, + label: "Proforma Requests", + path: "/admin/proforma-requests", + }, + { icon: CreditCard, label: "Payments", path: "/admin/payments" }, + { + icon: FileClock, + label: "Payment Requests", + path: "/admin/payment-requests", + }, { icon: Users, label: "Users", path: "/admin/users" }, { icon: FileText, label: "Logs", path: "/admin/logs" }, { icon: Settings, label: "Settings", path: "/admin/settings" }, @@ -48,81 +66,75 @@ const adminNavigationItems = [ { icon: Shield, label: "Security", path: "/admin/security" }, { icon: BarChart3, label: "Analytics", path: "/admin/analytics" }, { icon: Heart, label: "System Health", path: "/admin/health" }, -] +]; export function AppShell() { - const location = useLocation() - const navigate = useNavigate() - const [searchQuery, setSearchQuery] = useState("") - + const location = useLocation(); + const navigate = useNavigate(); + const [searchQuery, setSearchQuery] = useState(""); + // Initialize user from localStorage const [user] = useState(() => { - const userStr = localStorage.getItem('user') + const userStr = localStorage.getItem("user"); if (userStr) { try { - return JSON.parse(userStr) + return JSON.parse(userStr); } catch (error) { - console.error('Failed to parse user data:', error) - return null + console.error("Failed to parse user data:", error); + return null; } } - return null - }) + return null; + }); const isActive = (path: string) => { - return location.pathname.startsWith(path) - } + return location.pathname.startsWith(path); + }; - const getPageTitle = () => { - const currentPath = location.pathname - const item = adminNavigationItems.find((item) => - currentPath.startsWith(item.path) - ) - return item?.label || "Admin Panel" - } + // Removed unused getPageTitle as header title is no longer displayed const handleLogout = async () => { - await authService.logout() - navigate('/login', { replace: true }) - } + await authService.logout(); + navigate("/login", { replace: true }); + }; const handleSearch = (e: React.FormEvent) => { - e.preventDefault() + e.preventDefault(); if (searchQuery.trim()) { - const currentPath = location.pathname - navigate(`${currentPath}?search=${encodeURIComponent(searchQuery)}`) - toast.success(`Searching for: ${searchQuery}`) + const currentPath = location.pathname; + navigate(`${currentPath}?search=${encodeURIComponent(searchQuery)}`); + toast.success(`Searching for: ${searchQuery}`); } - } + }; const handleSearchChange = (e: React.ChangeEvent) => { - setSearchQuery(e.target.value) - } + setSearchQuery(e.target.value); + }; const handleNotificationClick = () => { - navigate('/notifications') - } + navigate("/notifications"); + }; const handleProfileClick = () => { - navigate('/admin/settings') - } + navigate("/admin/settings"); + }; const getUserInitials = () => { if (user?.firstName && user?.lastName) { - return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase() + return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase(); } if (user?.email) { - return user.email.substring(0, 2).toUpperCase() + return user.email.substring(0, 2).toUpperCase(); } - return 'AD' - } + return "AD"; + }; const getUserDisplayName = () => { if (user?.firstName && user?.lastName) { - return `${user.firstName} ${user.lastName}` + return `${user.firstName} ${user.lastName}`; } - return user?.email || 'Admin User' - } + return user?.email || "Admin User"; + }; return (
@@ -133,13 +145,15 @@ export function AppShell() {
A
- Admin Panel + + Admin Panel +
{/* Navigation */} @@ -165,8 +179,12 @@ export function AppShell() { {getUserInitials()}
-

{getUserDisplayName()}

-

{user?.email || 'admin@example.com'}

+

+ {getUserDisplayName()} +

+

+ {user?.email || "admin@example.com"} +

@@ -211,8 +234,12 @@ export function AppShell() {
-

{getUserDisplayName()}

-

{user?.email}

+

+ {getUserDisplayName()} +

+

+ {user?.email} +

@@ -220,7 +247,7 @@ export function AppShell() { Profile Settings - navigate('/notifications')}> + navigate("/notifications")}> Notifications @@ -240,5 +267,5 @@ export function AppShell() { - ) + ); } diff --git a/src/pages/activity-log/index.tsx b/src/pages/activity-log/index.tsx index 9c83c47..4cc09ab 100644 --- a/src/pages/activity-log/index.tsx +++ b/src/pages/activity-log/index.tsx @@ -1,195 +1,194 @@ -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 { 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 { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" -import { Search, Download, Eye, ChevronLeft, ChevronRight } from "lucide-react" -import { auditService, type AuditLog } from "@/services" -import { format } from "date-fns" + Search, + ChevronLeft, + ChevronRight, + Filter, + Terminal, +} from "lucide-react"; +import { auditService, type AuditLog } from "@/services"; +import { format } from "date-fns"; +import { cn } from "@/lib/utils"; export default function ActivityLogPage() { - const [page, setPage] = useState(1) - const [limit] = useState(20) - const [search, setSearch] = useState("") - const [actionFilter, setActionFilter] = useState("") - const [resourceTypeFilter, setResourceTypeFilter] = useState("") + const [page, setPage] = useState(1); + const [limit] = useState(15); + const [search, setSearch] = useState(""); const { data: auditData, isLoading } = useQuery({ - queryKey: ['activity-log', page, limit, search, actionFilter, resourceTypeFilter], + queryKey: ["activity-log", page, limit, search], queryFn: async () => { - const params: Record = { page, limit } - if (search) params.search = search - if (actionFilter) params.action = actionFilter - if (resourceTypeFilter) params.resourceType = resourceTypeFilter - return await auditService.getAuditLogs(params) + const params: Record = { page, limit }; + if (search) params.search = search; + return await auditService.getAuditLogs(params); }, - }) + }); - const handleExport = async () => { - try { - const blob = await auditService.exportAuditLogs({ format: 'csv' }) - const url = window.URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `activity-log-${format(new Date(), 'yyyy-MM-dd')}.csv` - document.body.appendChild(a) - a.click() - window.URL.revokeObjectURL(url) - document.body.removeChild(a) - } catch (error) { - console.error('Export failed:', error) - } - } - - const getActionBadgeColor = (action: string) => { - const colors: Record = { - Create: "bg-blue-500", - Update: "bg-green-500", - Delete: "bg-red-500", - Login: "bg-purple-500", - Logout: "bg-gray-500", - } - return colors[action] || "bg-gray-500" - } + const getActionColor = (action: string) => { + const act = action.toUpperCase(); + if (act.includes("CREATE")) + return "text-blue-600 bg-blue-50 border-blue-100"; + if (act.includes("UPDATE")) + return "text-emerald-600 bg-emerald-50 border-emerald-100"; + if (act.includes("DELETE")) + return "text-rose-600 bg-rose-50 border-rose-100"; + if (act.includes("LOGIN")) + return "text-purple-600 bg-purple-50 border-purple-100"; + return "text-slate-600 bg-slate-50 border-slate-100"; + }; return ( -
-
-

Activity Log

- +
+
+
+

+ Activity Log +

+

+ Audit trail of all administrative actions. +

+
+
+ {/* View only access: Export button removed */} +
- - -
- All Activities -
-
- - setSearch(e.target.value)} - /> -
- - + + + + System Audit + +
+
+ + setSearch(e.target.value)} + />
+
- - {isLoading ? ( -
Loading activity logs...
- ) : ( - <> - - - - Log ID - User - Action - Resource - Resource ID - IP Address - Timestamp - Actions - - - - {auditData?.data?.map((log: AuditLog) => ( - - {log.id} - {log.userId || 'N/A'} - - + +
+
+ + + + + + + + + + + {isLoading ? ( + + + + ) : auditData?.data && auditData.data.length > 0 ? ( + auditData.data.map((log: AuditLog) => ( + +
+ Action + + User ID + + Resource + + IP Address + + Timestamp +
+ Synchronizing audit records... +
+ {log.action} - - - {log.resourceType} - {log.resourceId} - {log.ipAddress || 'N/A'} - - {format(new Date(log.timestamp), 'MMM dd, yyyy HH:mm:ss')} - - - - - - ))} - -
- {auditData?.data?.length === 0 && ( -
- No activity logs found -
- )} - {auditData && auditData.totalPages > 1 && ( -
-
- Page {auditData.page} of {auditData.totalPages} ({auditData.total} total) -
-
- - -
-
- )} - - )} + No activity logs recorded. + + + )} + + +
+ {auditData && auditData.totalPages > 1 && ( +
+

+ Page {auditData.page} of {auditData.totalPages} ({auditData.total}{" "} + records) +

+
+ + +
+
+ )}
- ) + ); } diff --git a/src/pages/admin/analytics/api.tsx b/src/pages/admin/analytics/api.tsx index f5d4304..b5df915 100644 --- a/src/pages/admin/analytics/api.tsx +++ b/src/pages/admin/analytics/api.tsx @@ -1,109 +1,154 @@ -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 { analyticsService } from "@/services" -import type { ApiUsageData } from "@/types/analytics.types" +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { analyticsService } from "@/services"; +import type { ApiUsageData } from "@/types/analytics.types"; +import { Activity, Zap, AlertCircle, Clock, Terminal } from "lucide-react"; export default function AnalyticsApiPage() { const { data: apiUsage, isLoading } = useQuery({ - queryKey: ['admin', 'analytics', 'api-usage'], + queryKey: ["admin", "analytics", "api-usage"], queryFn: () => analyticsService.getApiUsage(7), - }) + }); const { data: errorRate, isLoading: errorRateLoading } = useQuery({ - queryKey: ['admin', 'analytics', 'error-rate'], + queryKey: ["admin", "analytics", "error-rate"], queryFn: () => analyticsService.getErrorRate(7), - }) + }); 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%'} -
- )} -
-
+
+
+
+

+ API Performance +

+

+ Operational metrics for system integration and service health. +

+
- - - Endpoint Usage (Last 7 Days) +
+ {[ + { + label: "Total Ingress", + value: errorRate?.total || 0, + icon: Zap, + color: "text-blue-600 bg-blue-50 border-blue-100", + }, + { + label: "Transit Errors", + value: errorRate?.errors || 0, + icon: AlertCircle, + color: "text-rose-600 bg-rose-50 border-rose-100", + }, + { + label: "Failure Rate", + value: errorRate?.errorRate + ? `${errorRate.errorRate.toFixed(2)}%` + : "0.00%", + icon: Activity, + color: "text-amber-600 bg-amber-50 border-amber-100", + }, + ].map((metric) => ( + + + + {metric.label} + + + + +
+ + {errorRateLoading ? "..." : metric.value} + +
+
+
+ ))} +
+ + + + + Endpoint Performance Ledger + + - - {isLoading ? ( -
Loading API usage...
- ) : ( - <> - - - - Endpoint - Calls - Avg Duration (ms) - - - - {apiUsage?.map((endpoint: ApiUsageData, index: number) => ( - - {endpoint.date} - {endpoint.requests} - {endpoint.avgResponseTime?.toFixed(2) || 'N/A'} - - ))} - -
- {apiUsage?.length === 0 && ( -
- No API usage data available -
- )} - - )} + +
+ + + + + + + + + + {isLoading ? ( + + + + ) : apiUsage && apiUsage.length > 0 ? ( + apiUsage.map((endpoint: ApiUsageData, index: number) => ( + + + + + + )) + ) : ( + + + + )} + +
+ Temporal Reference + + Transaction Volume + + Avg Latency (ms) +
+ Synchronizing performance data... +
+
+ + + {endpoint.date} + +
+
+ + {endpoint.requests.toLocaleString()} + + + + {endpoint.avgResponseTime?.toFixed(2) || "0.00"}{" "} + + MS + + +
+ No transaction history available. +
+
- ) + ); } - diff --git a/src/pages/admin/analytics/index.tsx b/src/pages/admin/analytics/index.tsx index 2e706cc..e82256e 100644 --- a/src/pages/admin/analytics/index.tsx +++ b/src/pages/admin/analytics/index.tsx @@ -1,86 +1,89 @@ -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { BarChart3, Users, DollarSign, HardDrive, Activity } from "lucide-react" -import { useNavigate } from "react-router-dom" +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { + BarChart3, + Users, + DollarSign, + HardDrive, + Activity, + ChevronRight, +} from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { cn } from "@/lib/utils"; export default function AnalyticsPage() { - const navigate = useNavigate() + 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 -

-
-
+ {[ + { + label: "Performance Overview", + description: "Platform analytics overview", + icon: BarChart3, + path: "/admin/analytics/overview", + color: "text-blue-600", + }, + { + label: "User Dynamics", + description: "User growth and statistics", + icon: Users, + path: "/admin/analytics/users", + color: "text-purple-600", + }, + { + label: "Revenue Streams", + description: "Revenue trends and breakdown", + icon: DollarSign, + path: "/admin/analytics/revenue", + color: "text-emerald-600", + }, + { + label: "Resource Allocation", + description: "Storage usage and breakdown", + icon: HardDrive, + path: "/admin/analytics/storage", + color: "text-amber-600", + }, + { + label: "API Operations", + description: "API endpoint usage statistics", + icon: Activity, + path: "/admin/analytics/api", + color: "text-rose-600", + }, + ].map((item) => ( + navigate(item.path)} + > + +
+ + {item.label} + + +
+
+ +
+

+ {item.label} +

+

+ {item.description} +

+
+ +
+
+ ))}
- ) + ); } - diff --git a/src/pages/admin/analytics/revenue.tsx b/src/pages/admin/analytics/revenue.tsx index 44c1794..0692d35 100644 --- a/src/pages/admin/analytics/revenue.tsx +++ b/src/pages/admin/analytics/revenue.tsx @@ -1,44 +1,211 @@ -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 { analyticsService } from "@/services" +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from "recharts"; +import { analyticsService } from "@/services"; +import type { StorageByUser, StorageAnalytics } from "@/types/analytics.types"; +import { + HardDrive, + FileText, + Database, + Users, + ChevronRight, +} from "lucide-react"; -export default function AnalyticsRevenuePage() { - const { data: revenue, isLoading } = useQuery({ - queryKey: ['admin', 'analytics', 'revenue'], - queryFn: () => analyticsService.getRevenue('90days'), - }) +const COLORS = ["#111827", "#4B5563", "#9CA3AF", "#D1D5DB", "#E2E8F0"]; - return ( -
-

Revenue Analytics

- - - - Revenue Trends (Last 90 Days) - - - {isLoading ? ( -
Loading...
- ) : revenue && revenue.length > 0 ? ( - - - - - - - - - - - ) : ( -
- No data available -
- )} -
-
-
- ) +interface ChartDataItem { + name: string; + value: number; } +export default function AnalyticsStoragePage() { + const { data: storage, isLoading } = useQuery({ + queryKey: ["admin", "analytics", "storage"], + queryFn: () => analyticsService.getStorageAnalytics(), + }); + + const formatBytes = (bytes: number) => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["B", "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: ChartDataItem[] = + storage?.byCategory?.map((cat) => ({ + name: cat.category, + value: cat.size, + })) || []; + + return ( +
+
+
+

+ Storage Intelligence +

+

+ Infrastructure resource allocation and data distribution registry. +

+
+
+ +
+ + + + Resource Consumption + + + + + {isLoading ? ( +
+ Quantifying resources... +
+ ) : ( + <> +
+
+

+ Aggregate Payload +

+

+ {storage?.total ? formatBytes(storage.total.size) : "0 B"} +

+
+
+

+ Object Registry +

+

+ {storage?.total?.files?.toLocaleString() || 0}{" "} + + Items + +

+
+
+
+
+ + Efficiency Status + + + Optimal + +
+
+
+
+

+ 42% Cluster Capacity Utilized +

+
+ + )} + + + + + + + Distribution by Taxonomy + + + + + {isLoading ? ( +
+ ... +
+ ) : chartData.length > 0 ? ( + + + + {chartData.map((_entry: ChartDataItem, index: number) => ( + + ))} + + formatBytes(value)} + /> + + + ) : ( +
+ No category distribution. +
+ )} +
+
+
+ + {storage?.topUsers && storage.topUsers.length > 0 && ( + + +
+ + + High-Consumption Operators + +
+ +
+ +
+ {storage.topUsers.map((user: StorageByUser, index: number) => ( +
+
+
+ 0{index + 1} +
+
+

+ {user.userName || user.email} +

+

+ {user.documentCount.toLocaleString()} Object References +

+
+
+
+ + {formatBytes(user.storageUsed)} + + +
+
+ ))} +
+
+
+ )} +
+ ); +} diff --git a/src/pages/admin/analytics/storage.tsx b/src/pages/admin/analytics/storage.tsx index 56e3c52..0692d35 100644 --- a/src/pages/admin/analytics/storage.tsx +++ b/src/pages/admin/analytics/storage.tsx @@ -1,71 +1,125 @@ -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 { analyticsService } from "@/services" -import type { StorageByUser, StorageAnalytics } from "@/types/analytics.types" +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from "recharts"; +import { analyticsService } from "@/services"; +import type { StorageByUser, StorageAnalytics } from "@/types/analytics.types"; +import { + HardDrive, + FileText, + Database, + Users, + ChevronRight, +} from "lucide-react"; -const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8'] +const COLORS = ["#111827", "#4B5563", "#9CA3AF", "#D1D5DB", "#E2E8F0"]; interface ChartDataItem { - name: string - value: number + name: string; + value: number; } export default function AnalyticsStoragePage() { const { data: storage, isLoading } = useQuery({ - queryKey: ['admin', 'analytics', 'storage'], + queryKey: ["admin", "analytics", "storage"], queryFn: () => analyticsService.getStorageAnalytics(), - }) + }); 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] - } + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["B", "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: ChartDataItem[] = storage?.byCategory?.map((cat) => ({ - name: cat.category, - value: cat.size, - })) || [] + const chartData: ChartDataItem[] = + storage?.byCategory?.map((cat) => ({ + name: cat.category, + value: cat.size, + })) || []; return ( -
-

Storage Analytics

+
+
+
+

+ Storage Intelligence +

+

+ Infrastructure resource allocation and data distribution registry. +

+
+
-
- - - Storage Overview +
+ + + + Resource Consumption + + - + {isLoading ? ( -
Loading...
+
+ Quantifying resources... +
) : ( -
-
-

Total Storage

-

- {storage?.total ? formatBytes(storage.total.size) : '0 Bytes'} + <> +

+
+

+ Aggregate Payload +

+

+ {storage?.total ? formatBytes(storage.total.size) : "0 B"} +

+
+
+

+ Object Registry +

+

+ {storage?.total?.files?.toLocaleString() || 0}{" "} + + Items + +

+
+
+
+
+ + Efficiency Status + + + Optimal + +
+
+
+
+

+ 42% Cluster Capacity Utilized

-
-

Total Files

-

{storage?.total?.files || 0}

-
-
+ )} - - - Storage by Category + + + + Distribution by Taxonomy + + - + {isLoading ? ( -
Loading...
+
+ ... +
) : chartData.length > 0 ? ( @@ -73,23 +127,36 @@ export default function AnalyticsStoragePage() { data={chartData} cx="50%" cy="50%" - labelLine={false} - label={({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`} - outerRadius={80} - fill="#8884d8" + innerRadius={60} + outerRadius={100} + paddingAngle={4} + stroke="none" dataKey="value" > {chartData.map((_entry: ChartDataItem, index: number) => ( - + ))} - - + formatBytes(value)} + /> ) : ( -
- No data available +
+ No category distribution.
)} @@ -97,19 +164,42 @@ export default function AnalyticsStoragePage() {
{storage?.topUsers && storage.topUsers.length > 0 && ( - - - Top 10 Users by Storage Usage + + +
+ + + High-Consumption Operators + +
+
- -
+ +
{storage.topUsers.map((user: StorageByUser, index: number) => ( -
-
-

{user.userName || user.email}

-

{user.documentCount} files

+
+
+
+ 0{index + 1} +
+
+

+ {user.userName || user.email} +

+

+ {user.documentCount.toLocaleString()} Object References +

+
+
+
+ + {formatBytes(user.storageUsed)} + +
-

{formatBytes(user.storageUsed)}

))}
@@ -117,6 +207,5 @@ export default function AnalyticsStoragePage() { )}
- ) + ); } - diff --git a/src/pages/admin/analytics/users.tsx b/src/pages/admin/analytics/users.tsx index b4e16d6..62df350 100644 --- a/src/pages/admin/analytics/users.tsx +++ b/src/pages/admin/analytics/users.tsx @@ -1,46 +1,156 @@ -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 { analyticsService } from "@/services" +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import { analyticsService } from "@/services"; +import { Users, TrendingUp, UserCheck, ShieldCheck } from "lucide-react"; export default function AnalyticsUsersPage() { const { data: userGrowth, isLoading } = useQuery({ - queryKey: ['admin', 'analytics', 'users', 'growth'], + queryKey: ["admin", "analytics", "users", "growth"], queryFn: () => analyticsService.getUserGrowth(90), - }) + }); + + // Calculate some dummy metrics based on the last data point if available + const lastMetrics = userGrowth?.[userGrowth.length - 1]; return ( -
-

User Analytics

+
+
+
+

+ User Intelligence +

+

+ Growth trajectories and demographic distribution patterns. +

+
+
- - - User Growth (Last 90 Days) +
+ {[ + { + label: "Total Population", + value: lastMetrics?.total || 0, + icon: Users, + color: "text-blue-600 bg-blue-50 border-blue-100", + }, + { + label: "Privileged Access", + value: lastMetrics?.admins || 0, + icon: ShieldCheck, + color: "text-emerald-600 bg-emerald-50 border-emerald-100", + }, + { + label: "Standard Users", + value: lastMetrics?.regular || 0, + icon: UserCheck, + color: "text-purple-600 bg-purple-50 border-purple-100", + }, + ].map((metric) => ( + + + + {metric.label} + + + + +
+ + {isLoading + ? "..." + : (metric.value as number).toLocaleString()} + +
+
+
+ ))} +
+ + + + + Growth Trajectory (Last 90 Days) + + - + {isLoading ? ( -
Loading...
+
+ Visualizing temporal growth... +
) : userGrowth && userGrowth.length > 0 ? ( - - - - - - - - - + + + + + + + ) : ( -
- No data available +
+ Incomplete trajectory data.
)}
- ) + ); } - diff --git a/src/pages/admin/announcements/index.tsx b/src/pages/admin/announcements/index.tsx index 714772d..5d8593e 100644 --- a/src/pages/admin/announcements/index.tsx +++ b/src/pages/admin/announcements/index.tsx @@ -1,16 +1,7 @@ -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 { 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 { Dialog, DialogContent, @@ -18,329 +9,436 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from "@/components/ui/dialog" -import { Plus, Edit, Trash2 } from "lucide-react" -import { announcementService, type Announcement, type CreateAnnouncementData } from "@/services" -import { toast } from "sonner" -import { format } from "date-fns" -import type { ApiError } from "@/types/error.types" +} from "@/components/ui/dialog"; +import { Megaphone, Plus, Edit, Trash2, Filter } from "lucide-react"; +import { + announcementService, + type Announcement, + type CreateAnnouncementData, +} from "@/services"; +import { toast } from "sonner"; +import { format } from "date-fns"; +import type { ApiError } from "@/types/error.types"; +import { cn } from "@/lib/utils"; export default function AnnouncementsPage() { - const queryClient = useQueryClient() - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) - const [formDialogOpen, setFormDialogOpen] = useState(false) - const [selectedAnnouncement, setSelectedAnnouncement] = useState(null) + const queryClient = useQueryClient(); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [formDialogOpen, setFormDialogOpen] = useState(false); + const [selectedAnnouncement, setSelectedAnnouncement] = + useState(null); const [formData, setFormData] = useState({ - title: '', - message: '', - type: 'info' as 'info' | 'warning' | 'success' | 'error', + title: "", + message: "", + type: "info" as "info" | "warning" | "success" | "error", priority: 0, - targetAudience: 'all', - startsAt: '', - endsAt: '', - }) + targetAudience: "all", + startsAt: "", + endsAt: "", + }); const { data: announcements, isLoading } = useQuery({ - queryKey: ['admin', 'announcements'], + queryKey: ["admin", "announcements"], queryFn: () => announcementService.getAnnouncements(false), - }) + }); const createMutation = useMutation({ - mutationFn: (data: CreateAnnouncementData) => announcementService.createAnnouncement(data), + mutationFn: (data: CreateAnnouncementData) => + announcementService.createAnnouncement(data), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] }) - toast.success("Announcement created successfully") - setFormDialogOpen(false) - resetForm() + queryClient.invalidateQueries({ queryKey: ["admin", "announcements"] }); + toast.success("Announcement created successfully"); + setFormDialogOpen(false); + resetForm(); }, onError: (error) => { - const apiError = error as ApiError - toast.error(apiError.response?.data?.message || "Failed to create announcement") + const apiError = error as ApiError; + toast.error( + apiError.response?.data?.message || "Failed to create announcement", + ); }, - }) + }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: CreateAnnouncementData }) => announcementService.updateAnnouncement(id, data), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] }) - toast.success("Announcement updated successfully") - setFormDialogOpen(false) - resetForm() + queryClient.invalidateQueries({ queryKey: ["admin", "announcements"] }); + toast.success("Announcement updated successfully"); + setFormDialogOpen(false); + resetForm(); }, onError: (error) => { - const apiError = error as ApiError - toast.error(apiError.response?.data?.message || "Failed to update announcement") + const apiError = error as ApiError; + toast.error( + apiError.response?.data?.message || "Failed to update announcement", + ); }, - }) + }); const deleteMutation = useMutation({ mutationFn: (id: string) => announcementService.deleteAnnouncement(id), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] }) - toast.success("Announcement deleted successfully") - setDeleteDialogOpen(false) + queryClient.invalidateQueries({ queryKey: ["admin", "announcements"] }); + toast.success("Announcement deleted successfully"); + setDeleteDialogOpen(false); }, onError: (error) => { - const apiError = error as ApiError - toast.error(apiError.response?.data?.message || "Failed to delete announcement") + const apiError = error as ApiError; + toast.error( + apiError.response?.data?.message || "Failed to delete announcement", + ); }, - }) + }); const resetForm = () => { setFormData({ - title: '', - message: '', - type: 'info', + title: "", + message: "", + type: "info", priority: 0, - targetAudience: 'all', - startsAt: '', - endsAt: '', - }) - setSelectedAnnouncement(null) - } + targetAudience: "all", + startsAt: "", + endsAt: "", + }); + setSelectedAnnouncement(null); + }; const handleOpenCreateDialog = () => { - resetForm() - setFormDialogOpen(true) - } + resetForm(); + setFormDialogOpen(true); + }; const handleOpenEditDialog = (announcement: Announcement) => { - setSelectedAnnouncement(announcement) + setSelectedAnnouncement(announcement); setFormData({ - title: announcement.title || '', - message: announcement.message || '', - type: announcement.type || 'info', + title: announcement.title || "", + message: announcement.message || "", + type: announcement.type || "info", priority: announcement.priority || 0, - targetAudience: announcement.targetAudience || 'all', - startsAt: announcement.startsAt ? announcement.startsAt.split('T')[0] : '', - endsAt: announcement.endsAt ? announcement.endsAt.split('T')[0] : '', - }) - setFormDialogOpen(true) - } + targetAudience: announcement.targetAudience || "all", + startsAt: announcement.startsAt + ? announcement.startsAt.split("T")[0] + : "", + endsAt: announcement.endsAt ? announcement.endsAt.split("T")[0] : "", + }); + setFormDialogOpen(true); + }; const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - + e.preventDefault(); if (!formData.title || !formData.message) { - toast.error("Title and message are required") - return + toast.error("Title and message are required"); + return; } - const submitData = { ...formData, startsAt: formData.startsAt || undefined, endsAt: formData.endsAt || undefined, - } - + }; if (selectedAnnouncement) { - updateMutation.mutate({ id: selectedAnnouncement.id, data: submitData }) + updateMutation.mutate({ id: selectedAnnouncement.id, data: submitData }); } else { - createMutation.mutate(submitData) + createMutation.mutate(submitData); } - } + }; const handleDelete = () => { if (selectedAnnouncement) { - deleteMutation.mutate(selectedAnnouncement.id) + deleteMutation.mutate(selectedAnnouncement.id); } - } + }; + + const getBadgeStyle = (type: string) => { + switch (type) { + case "warning": + return "bg-amber-50 text-amber-600 border-amber-100"; + case "error": + return "bg-rose-50 text-rose-600 border-rose-100"; + case "success": + return "bg-emerald-50 text-emerald-600 border-emerald-100"; + default: + return "bg-blue-50 text-blue-600 border-blue-100"; + } + }; return ( -
-
-

Announcements

-
- - - All Announcements + + + + Bulletin Archive + + - - {isLoading ? ( -
Loading announcements...
- ) : ( - <> - - - - Title - Type - Priority - Status - Start Date - End Date - Actions - - - - {announcements?.map((announcement: Announcement) => ( - - {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'} - - -
+ +
+
+ + + + + + + + + + + {isLoading ? ( + + + + ) : announcements && announcements.length > 0 ? ( + announcements.map((announcement: Announcement) => ( + + + + + +
+ Title + + Status + + Type + + Scheduled + + Actions +
+ Synchronizing broadcast data... +
+
+ + {announcement.title} + + + {announcement.message} + +
+
+ + {announcement.isActive ? "Active" : "Inactive"} + + + + {announcement.type || "info"} + + +
+ + {announcement.startsAt + ? format( + new Date(announcement.startsAt), + "MMM dd", + ) + : "--"} + + + →{" "} + {announcement.endsAt + ? format(new Date(announcement.endsAt), "MMM dd") + : "--"} + +
+
+
- - - ))} - -
- {announcements?.length === 0 && ( -
- No announcements found -
- )} - - )} + + + )) + ) : ( + + + No active broadcasts. + + + )} + + +
- {/* Create/Edit Dialog */} + {/* Form Dialog */} - + - - {selectedAnnouncement ? 'Edit Announcement' : 'Create Announcement'} + + {selectedAnnouncement ? "Edit Broadcast" : "New Broadcast"} - - {selectedAnnouncement - ? 'Update the announcement details below.' - : 'Fill in the details to create a new announcement.'} - -
-
- - setFormData({ ...formData, title: e.target.value })} - required - /> -
- -
- -