Compare commits

..

No commits in common. "20b02512597b7ec049bca30d7580e03e6331aa88" and "d7c860df17837274bfe61cbe42c4e9d4d84b943a" have entirely different histories.

34 changed files with 131 additions and 4815 deletions

1248
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,27 +11,17 @@
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@tanstack/react-query": "^5.90.12",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.561.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.11.0",
"recharts": "^3.6.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {

View File

@ -1,63 +1,26 @@
import { Navigate, Route, Routes } from "react-router-dom"
import { AppShell } from "@/layouts/app-shell"
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 LogsPage from "@/pages/admin/logs"
import ErrorLogsPage from "@/pages/admin/logs/errors"
import AccessLogsPage from "@/pages/admin/logs/access"
import LogDetailsPage from "@/pages/admin/logs/[id]"
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 DashboardPage from "@/pages/dashboard"
import SubscriptionsPage from "@/pages/subscriptions"
import TransactionsPage from "@/pages/transactions"
import ClientsPage from "@/pages/clients"
import MembersPage from "@/pages/members"
import NotificationsPage from "@/pages/notifications"
import ActivityLogPage from "@/pages/activity-log"
function App() {
return (
<Routes>
<Route element={<AppShell />}>
<Route index element={<Navigate to="/admin/dashboard" replace />} />
<Route path="admin/dashboard" element={<DashboardPage />} />
<Route path="admin/users" element={<UsersPage />} />
<Route path="admin/users/:id" element={<UserDetailsPage />} />
<Route path="admin/users/:id/activity" element={<UserActivityPage />} />
<Route path="admin/logs" element={<LogsPage />} />
<Route path="admin/logs/errors" element={<ErrorLogsPage />} />
<Route path="admin/logs/access" element={<AccessLogsPage />} />
<Route path="admin/logs/:id" element={<LogDetailsPage />} />
<Route path="admin/settings" element={<SettingsPage />} />
<Route path="admin/maintenance" element={<MaintenancePage />} />
<Route path="admin/announcements" element={<AnnouncementsPage />} />
<Route path="admin/audit" element={<AuditPage />} />
<Route path="admin/security" element={<SecurityPage />} />
<Route path="admin/security/failed-logins" element={<FailedLoginsPage />} />
<Route path="admin/security/suspicious" element={<SuspiciousActivityPage />} />
<Route path="admin/security/api-keys" element={<ApiKeysPage />} />
<Route path="admin/security/rate-limits" element={<RateLimitsPage />} />
<Route path="admin/security/sessions" element={<SessionsPage />} />
<Route path="admin/analytics" element={<AnalyticsPage />} />
<Route path="admin/analytics/overview" element={<AnalyticsOverviewPage />} />
<Route path="admin/analytics/users" element={<AnalyticsUsersPage />} />
<Route path="admin/analytics/revenue" element={<AnalyticsRevenuePage />} />
<Route path="admin/analytics/storage" element={<AnalyticsStoragePage />} />
<Route path="admin/analytics/api" element={<AnalyticsApiPage />} />
<Route path="admin/health" element={<HealthPage />} />
<Route index element={<DashboardPage />} />
<Route path="subscriptions" element={<SubscriptionsPage />} />
<Route path="transactions" element={<TransactionsPage />} />
<Route path="clients" element={<ClientsPage />} />
<Route path="members" element={<MembersPage />} />
<Route path="notifications" element={<NotificationsPage />} />
<Route path="activity-log" element={<ActivityLogPage />} />
</Route>
<Route path="*" element={<Navigate to="/admin/dashboard" replace />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
)
}

View File

@ -1,99 +0,0 @@
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Calendar, X } from "lucide-react"
import { format } from "date-fns"
interface DateRangePickerProps {
startDate?: string
endDate?: string
onDateRangeChange: (startDate?: string, endDate?: string) => void
className?: string
}
export function DateRangePicker({ startDate, endDate, onDateRangeChange, className }: DateRangePickerProps) {
const [open, setOpen] = useState(false)
const [localStartDate, setLocalStartDate] = useState(startDate || "")
const [localEndDate, setLocalEndDate] = useState(endDate || "")
const handleApply = () => {
onDateRangeChange(localStartDate || undefined, localEndDate || undefined)
setOpen(false)
}
const handleClear = () => {
setLocalStartDate("")
setLocalEndDate("")
onDateRangeChange(undefined, undefined)
setOpen(false)
}
const displayText = () => {
if (startDate && endDate) {
try {
const start = format(new Date(startDate), "MMM dd, yyyy")
const end = format(new Date(endDate), "MMM dd, yyyy")
return `${start} - ${end}`
} catch {
return "Invalid date range"
}
}
return "Select date range"
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" className={className}>
<Calendar className="w-4 h-4 mr-2" />
{displayText()}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Select Date Range</DialogTitle>
<DialogDescription>
Choose a start and end date to filter logs
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="start-date">Start Date</Label>
<Input
id="start-date"
type="date"
value={localStartDate}
onChange={(e) => setLocalStartDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="end-date">End Date</Label>
<Input
id="end-date"
type="date"
value={localEndDate}
onChange={(e) => setLocalEndDate(e.target.value)}
/>
</div>
<div className="flex items-center justify-between">
<Button variant="outline" onClick={handleClear}>
<X className="w-4 h-4 mr-2" />
Clear
</Button>
<Button onClick={handleApply}>Apply</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@ -1,121 +0,0 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -1,25 +0,0 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -1,158 +0,0 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -1,28 +0,0 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@ -1,54 +0,0 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -1,27 +0,0 @@
import * as React from "react"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@ -1,19 +1,21 @@
import { Outlet, Link, useLocation } from "react-router-dom"
import {
LayoutDashboard,
Users,
FileText,
FolderKanban,
Calendar,
Leaf,
Settings,
Wrench,
Megaphone,
Shield,
BarChart3,
Activity,
Heart,
Search,
Mail,
Bell,
LogOut,
HelpCircle,
DollarSign,
FileText,
Users,
Briefcase,
Mail,
Copy,
UserPlus,
Megaphone,
Search,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@ -22,32 +24,33 @@ import { Separator } from "@/components/ui/separator"
import { Card, CardContent } from "@/components/ui/card"
import { cn } from "@/lib/utils"
const navigationItems = [
{ icon: LayoutDashboard, label: "Dashboard", path: "/" },
{ icon: FolderKanban, label: "Projects", path: "/projects" },
{ icon: Calendar, label: "Calendar", path: "/calendar" },
{ icon: Leaf, label: "Leave Management", path: "/leave" },
{ icon: Settings, label: "Settings", path: "/settings" },
{ icon: Bell, label: "Notification", path: "/notifications" },
{ icon: HelpCircle, label: "Help & Center", path: "/help" },
]
const adminNavigationItems = [
{ icon: LayoutDashboard, label: "Dashboard", path: "/admin/dashboard" },
{ icon: Users, label: "Users", path: "/admin/users" },
{ icon: FileText, label: "Logs", path: "/admin/logs" },
{ icon: Settings, label: "Settings", path: "/admin/settings" },
{ icon: Wrench, label: "Maintenance", path: "/admin/maintenance" },
{ icon: Megaphone, label: "Announcements", path: "/admin/announcements" },
{ icon: Activity, label: "Audit", path: "/admin/audit" },
{ icon: Shield, label: "Security", path: "/admin/security" },
{ icon: BarChart3, label: "Analytics", path: "/admin/analytics" },
{ icon: Heart, label: "System Health", path: "/admin/health" },
{ icon: Users, label: "Subscriptions", path: "/subscriptions" },
{ icon: DollarSign, label: "Transactions", path: "/transactions" },
{ icon: Briefcase, label: "Clients", path: "/clients" },
{ icon: Users, label: "Members", path: "/members" },
{ icon: Bell, label: "Notifications", path: "/notifications" },
{ icon: FileText, label: "Activity Log", path: "/activity-log" },
]
export function AppShell() {
const location = useLocation()
const isActive = (path: string) => {
return location.pathname.startsWith(path)
if (path === "/") {
return location.pathname === "/"
}
const getPageTitle = () => {
const currentPath = location.pathname
const item = adminNavigationItems.find((item) =>
currentPath.startsWith(item.path)
)
return item?.label || "Admin Panel"
return location.pathname.startsWith(path)
}
return (
@ -57,13 +60,38 @@ export function AppShell() {
{/* Logo */}
<div className="p-6 flex items-center gap-2">
<div className="w-10 h-10 bg-primary rounded flex items-center justify-center">
<span className="text-primary-foreground font-bold text-lg">A</span>
<span className="text-primary-foreground font-bold text-lg">H</span>
</div>
<span className="text-foreground font-semibold text-lg">Admin Panel</span>
<span className="text-foreground font-semibold text-lg">TurHR</span>
</div>
{/* Navigation */}
<nav className="flex-1 px-4 py-2 space-y-1 overflow-y-auto">
<nav className="flex-1 px-4 py-2 space-y-1">
{navigationItems.map((item) => {
const Icon = item.icon
return (
<Link
key={item.path}
to={item.path}
className={cn(
"flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors",
isActive(item.path)
? "bg-primary text-primary-foreground"
: "text-foreground/70 hover:bg-accent hover:text-foreground"
)}
>
<Icon className="w-5 h-5" />
{item.label}
</Link>
)
})}
<Separator className="my-4" />
<div className="text-xs font-semibold text-foreground/50 uppercase tracking-wider px-3 py-2">
Admin
</div>
{adminNavigationItems.map((item) => {
const Icon = item.icon
return (
@ -84,29 +112,31 @@ export function AppShell() {
})}
</nav>
{/* User Section */}
<div className="p-4 border-t">
<div className="flex items-center gap-3 mb-3">
<Avatar>
<AvatarFallback>AD</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">Admin User</p>
<p className="text-xs text-muted-foreground truncate">admin@example.com</p>
{/* Announcements Card */}
<div className="p-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<Megaphone className="w-4 h-4 text-primary" />
<span className="font-semibold text-sm">Announcements</span>
</div>
</div>
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => {
localStorage.removeItem('access_token')
window.location.href = '/login'
}}
>
<LogOut className="w-4 h-4 mr-2" />
Logout
<p className="text-xs text-muted-foreground mb-3">
Stay updated with TurHR
</p>
<Button size="sm" className="w-full">
Create Now
</Button>
<div className="mt-3 text-xs text-muted-foreground">
Read by:
</div>
<div className="flex items-center gap-1 mt-1">
<div className="w-6 h-6 rounded-full bg-primary/20" />
<div className="w-6 h-6 rounded-full bg-primary/20" />
<div className="w-6 h-6 rounded-full bg-primary/20" />
<span className="text-xs text-muted-foreground ml-1">+10</span>
</div>
</CardContent>
</Card>
</div>
</aside>
@ -114,7 +144,7 @@ export function AppShell() {
<div className="flex-1 flex flex-col overflow-hidden">
{/* Top Header */}
<header className="h-16 border-b bg-background flex items-center justify-between px-6">
<h1 className="text-2xl font-bold">{getPageTitle()}</h1>
<h1 className="text-2xl font-bold">Dashboard</h1>
<div className="flex items-center gap-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
@ -127,12 +157,21 @@ export function AppShell() {
<Mail className="w-5 h-5" />
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full" />
</Button>
<Button variant="ghost" size="icon">
<Copy className="w-5 h-5" />
</Button>
<Button variant="ghost" size="icon" className="relative">
<Bell className="w-5 h-5" />
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full" />
<Users className="w-5 h-5" />
<span className="absolute top-0 right-0 bg-primary text-primary-foreground text-xs px-1 rounded">
+10
</span>
</Button>
<Button>
<UserPlus className="w-4 h-4 mr-2" />
Invite
</Button>
<Avatar>
<AvatarFallback>AD</AvatarFallback>
<AvatarFallback>U</AvatarFallback>
</Avatar>
</div>
</header>

View File

@ -1,277 +0,0 @@
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';
} else if (error.response?.status === 403) {
// Show access denied
const message = error.response?.data?.message || 'You do not have permission to access this resource';
if (typeof window !== 'undefined') {
import('sonner').then(({ toast }) => {
toast.error(message);
});
}
} else if (error.response?.status === 404) {
const message = error.response?.data?.message || 'Resource not found';
if (typeof window !== 'undefined') {
import('sonner').then(({ toast }) => {
toast.error(message);
});
}
} else if (error.response?.status === 500) {
const message = error.response?.data?.message || 'Server error occurred. Please try again later.';
if (typeof window !== 'undefined') {
import('sonner').then(({ toast }) => {
toast.error(message);
});
}
} else if (!error.response) {
// Network error
if (typeof window !== 'undefined') {
import('sonner').then(({ toast }) => {
toast.error('Network error. Please check your connection.');
});
}
}
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;

View File

@ -5,14 +5,12 @@ 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(
<StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
<Toaster />
</BrowserRouter>
</QueryClientProvider>
</StrictMode>,

View File

@ -1,114 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">API Usage Analytics</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Total API Calls</CardTitle>
</CardHeader>
<CardContent>
{errorRateLoading ? (
<div className="text-2xl font-bold">...</div>
) : (
<div className="text-2xl font-bold">{errorRate?.total || 0}</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Errors</CardTitle>
</CardHeader>
<CardContent>
{errorRateLoading ? (
<div className="text-2xl font-bold">...</div>
) : (
<div className="text-2xl font-bold">{errorRate?.errors || 0}</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Error Rate</CardTitle>
</CardHeader>
<CardContent>
{errorRateLoading ? (
<div className="text-2xl font-bold">...</div>
) : (
<div className="text-2xl font-bold">
{errorRate?.errorRate ? `${errorRate.errorRate.toFixed(2)}%` : '0%'}
</div>
)}
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Endpoint Usage (Last 7 Days)</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">Loading API usage...</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Endpoint</TableHead>
<TableHead>Calls</TableHead>
<TableHead>Avg Duration (ms)</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiUsage?.map((endpoint: any, index: number) => (
<TableRow key={index}>
<TableCell className="font-mono text-sm">{endpoint.endpoint}</TableCell>
<TableCell>{endpoint.calls}</TableCell>
<TableCell>{endpoint.avgDuration?.toFixed(2) || 'N/A'}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{apiUsage?.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No API usage data available
</div>
)}
</>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,87 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">Analytics</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card className="cursor-pointer hover:bg-accent transition-colors" onClick={() => navigate('/admin/analytics/overview')}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="w-5 h-5" />
Overview
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Platform analytics overview
</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:bg-accent transition-colors" onClick={() => navigate('/admin/analytics/users')}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="w-5 h-5" />
Users Analytics
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
User growth and statistics
</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:bg-accent transition-colors" onClick={() => navigate('/admin/analytics/revenue')}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="w-5 h-5" />
Revenue Analytics
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Revenue trends and breakdown
</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:bg-accent transition-colors" onClick={() => navigate('/admin/analytics/storage')}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<HardDrive className="w-5 h-5" />
Storage Analytics
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Storage usage and breakdown
</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:bg-accent transition-colors" onClick={() => navigate('/admin/analytics/api')}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="w-5 h-5" />
API Usage
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
API endpoint usage statistics
</p>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@ -1,6 +0,0 @@
import DashboardPage from "../../dashboard"
export default function AnalyticsOverviewPage() {
return <DashboardPage />
}

View File

@ -1,47 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">Revenue Analytics</h2>
<Card>
<CardHeader>
<CardTitle>Revenue Trends (Last 90 Days)</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="h-[400px] flex items-center justify-center">Loading...</div>
) : revenue && revenue.length > 0 ? (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={revenue}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="revenue" fill="#8884d8" name="Revenue" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-[400px] flex items-center justify-center text-muted-foreground">
No data available
</div>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,119 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">Storage Analytics</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Storage Overview</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="h-[300px] flex items-center justify-center">Loading...</div>
) : (
<div className="space-y-4">
<div>
<p className="text-sm text-muted-foreground">Total Storage</p>
<p className="text-2xl font-bold">
{storage?.total ? formatBytes(storage.total.size) : '0 Bytes'}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Total Files</p>
<p className="text-2xl font-bold">{storage?.total?.files || 0}</p>
</div>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Storage by Category</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="h-[300px] flex items-center justify-center">Loading...</div>
) : chartData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{chartData.map((entry: any, index: number) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
No data available
</div>
)}
</CardContent>
</Card>
</div>
{storage?.topUsers && storage.topUsers.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Top 10 Users by Storage Usage</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{storage.topUsers.map((user: any, index: number) => (
<div key={index} className="flex items-center justify-between p-2 border rounded">
<div>
<p className="font-medium">{user.user}</p>
<p className="text-sm text-muted-foreground">{user.files} files</p>
</div>
<p className="font-medium">{formatBytes(user.size)}</p>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
)
}

View File

@ -1,49 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">User Analytics</h2>
<Card>
<CardHeader>
<CardTitle>User Growth (Last 90 Days)</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="h-[400px] flex items-center justify-center">Loading...</div>
) : userGrowth && userGrowth.length > 0 ? (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={userGrowth}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="total" stroke="#8884d8" name="Total Users" />
<Line type="monotone" dataKey="admins" stroke="#82ca9d" name="Admins" />
<Line type="monotone" dataKey="regular" stroke="#ffc658" name="Regular Users" />
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-[400px] flex items-center justify-center text-muted-foreground">
No data available
</div>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,166 +0,0 @@
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<any>(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 (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold">Announcements</h2>
<Button onClick={() => setCreateDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Create Announcement
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>All Announcements</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">Loading announcements...</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Type</TableHead>
<TableHead>Priority</TableHead>
<TableHead>Status</TableHead>
<TableHead>Start Date</TableHead>
<TableHead>End Date</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{announcements?.map((announcement: any) => (
<TableRow key={announcement.id}>
<TableCell className="font-medium">{announcement.title}</TableCell>
<TableCell>
<Badge>{announcement.type || 'info'}</Badge>
</TableCell>
<TableCell>{announcement.priority || 0}</TableCell>
<TableCell>
<Badge variant={announcement.isActive ? 'default' : 'secondary'}>
{announcement.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>
{announcement.startsAt ? format(new Date(announcement.startsAt), 'MMM dd, yyyy') : 'N/A'}
</TableCell>
<TableCell>
{announcement.endsAt ? format(new Date(announcement.endsAt), 'MMM dd, yyyy') : 'N/A'}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon">
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => {
setSelectedAnnouncement(announcement)
setDeleteDialogOpen(true)
}}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{announcements?.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No announcements found
</div>
)}
</>
)}
</CardContent>
</Card>
{/* Delete Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Announcement</DialogTitle>
<DialogDescription>
Are you sure you want to delete "{selectedAnnouncement?.title}"? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
Cancel
</Button>
<Button variant="destructive" onClick={handleDelete}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@ -1,106 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">Audit Logs</h2>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>All Audit Logs</CardTitle>
<div className="flex items-center gap-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search audit logs..."
className="pl-10 w-64"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">Loading audit logs...</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Action</TableHead>
<TableHead>Resource Type</TableHead>
<TableHead>Resource ID</TableHead>
<TableHead>User</TableHead>
<TableHead>IP Address</TableHead>
<TableHead>Date</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{auditData?.data?.map((log: any) => (
<TableRow key={log.id}>
<TableCell>
<Badge>{log.action}</Badge>
</TableCell>
<TableCell>{log.resourceType}</TableCell>
<TableCell className="font-mono text-sm">{log.resourceId}</TableCell>
<TableCell>{log.userId || 'N/A'}</TableCell>
<TableCell>{log.ipAddress || 'N/A'}</TableCell>
<TableCell>
{format(new Date(log.createdAt), 'MMM dd, yyyy HH:mm')}
</TableCell>
<TableCell>
<Button variant="ghost" size="icon">
<Eye className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{auditData?.data?.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No audit logs found
</div>
)}
</>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,306 +0,0 @@
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 { data: errorRate, isLoading: errorRateLoading } = useQuery({
queryKey: ['admin', 'analytics', 'error-rate'],
queryFn: async () => {
const response = await adminApiHelpers.getErrorRate(7)
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 (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold">Dashboard Overview</h2>
<div className="flex items-center gap-4">
<div className="text-sm text-muted-foreground">
{new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</div>
<Button variant="outline" onClick={handleExport}>
<Download className="w-4 h-4 mr-2" />
Export Data
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{overviewLoading ? (
<div className="text-2xl font-bold">...</div>
) : (
<>
<div className="text-2xl font-bold">{overview?.users?.total || 0}</div>
<p className="text-xs text-muted-foreground">
{overview?.users?.active || 0} active, {overview?.users?.inactive || 0} inactive
</p>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Invoices</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{overviewLoading ? (
<div className="text-2xl font-bold">...</div>
) : (
<>
<div className="text-2xl font-bold">{overview?.invoices?.total || 0}</div>
<p className="text-xs text-muted-foreground">
All time invoices
</p>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{overviewLoading ? (
<div className="text-2xl font-bold">...</div>
) : (
<>
<div className="text-2xl font-bold">
{overview?.revenue ? formatCurrency(overview.revenue.total) : '$0.00'}
</div>
<p className="text-xs text-muted-foreground">
Total revenue
</p>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Storage Usage</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{overviewLoading ? (
<div className="text-2xl font-bold">...</div>
) : (
<>
<div className="text-2xl font-bold">
{overview?.storage ? formatBytes(overview.storage.totalSize) : '0 Bytes'}
</div>
<p className="text-xs text-muted-foreground">
{overview?.storage?.documents || 0} documents
</p>
</>
)}
</CardContent>
</Card>
</div>
{/* Charts Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* User Growth Chart */}
<Card>
<CardHeader>
<CardTitle>User Growth (Last 30 Days)</CardTitle>
</CardHeader>
<CardContent>
{growthLoading ? (
<div className="h-[300px] flex items-center justify-center">Loading...</div>
) : userGrowth && userGrowth.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={userGrowth}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="total" stroke="#8884d8" name="Total Users" />
<Line type="monotone" dataKey="admins" stroke="#82ca9d" name="Admins" />
<Line type="monotone" dataKey="regular" stroke="#ffc658" name="Regular Users" />
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
No data available
</div>
)}
</CardContent>
</Card>
{/* Revenue Chart */}
<Card>
<CardHeader>
<CardTitle>Revenue Analytics (Last 30 Days)</CardTitle>
</CardHeader>
<CardContent>
{revenueLoading ? (
<div className="h-[300px] flex items-center justify-center">Loading...</div>
) : revenue && revenue.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={revenue}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="revenue" fill="#8884d8" name="Revenue" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
No data available
</div>
)}
</CardContent>
</Card>
</div>
{/* Error Rate Chart */}
{errorRate && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertCircle className="h-5 w-5" />
Error Rate (Last 7 Days)
</CardTitle>
</CardHeader>
<CardContent>
{errorRateLoading ? (
<div className="h-[200px] flex items-center justify-center">Loading...</div>
) : (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div>
<p className="text-sm text-muted-foreground">Total Errors</p>
<p className="text-2xl font-bold">{errorRate.errors || 0}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Total Requests</p>
<p className="text-2xl font-bold">{errorRate.total || 0}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Error Rate</p>
<p className="text-2xl font-bold">
{errorRate.errorRate ? `${errorRate.errorRate.toFixed(2)}%` : '0%'}
</p>
</div>
</div>
<div className="w-full bg-muted rounded-full h-4">
<div
className="bg-destructive h-4 rounded-full transition-all"
style={{
width: `${Math.min(errorRate.errorRate || 0, 100)}%`,
}}
/>
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* System Health */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertCircle className="h-5 w-5" />
System Health
</CardTitle>
</CardHeader>
<CardContent>
{healthLoading ? (
<div>Loading system health...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<p className="text-sm text-muted-foreground">Status</p>
<p className="text-lg font-semibold capitalize">{health?.status || 'Unknown'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Database</p>
<p className="text-lg font-semibold capitalize">{health?.database || 'Unknown'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Recent Errors</p>
<p className="text-lg font-semibold">{health?.recentErrors || 0}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Active Users</p>
<p className="text-lg font-semibold">{health?.activeUsers || 0}</p>
</div>
</div>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,183 +0,0 @@
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 <CheckCircle className="w-5 h-5 text-green-500" />
case 'degraded':
return <AlertCircle className="w-5 h-5 text-yellow-500" />
case 'disconnected':
case 'down':
return <XCircle className="w-5 h-5 text-red-500" />
default:
return <AlertCircle className="w-5 h-5 text-gray-500" />
}
}
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">System Health</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{getStatusIcon(health?.status)}
System Status
</CardTitle>
</CardHeader>
<CardContent>
{healthLoading ? (
<div>Loading...</div>
) : (
<Badge variant={health?.status === 'healthy' ? 'default' : 'destructive'}>
{health?.status || 'Unknown'}
</Badge>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{getStatusIcon(health?.database)}
Database
</CardTitle>
</CardHeader>
<CardContent>
{healthLoading ? (
<div>Loading...</div>
) : (
<Badge variant={health?.database === 'connected' ? 'default' : 'destructive'}>
{health?.database || 'Unknown'}
</Badge>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertCircle className="w-5 h-5" />
Recent Errors
</CardTitle>
</CardHeader>
<CardContent>
{healthLoading ? (
<div>Loading...</div>
) : (
<div className="text-2xl font-bold">{health?.recentErrors || 0}</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="w-5 h-5" />
Active Users
</CardTitle>
</CardHeader>
<CardContent>
{healthLoading ? (
<div>Loading...</div>
) : (
<div className="text-2xl font-bold">{health?.activeUsers || 0}</div>
)}
</CardContent>
</Card>
</div>
{systemInfo && (
<Card>
<CardHeader>
<CardTitle>System Information</CardTitle>
</CardHeader>
<CardContent>
{infoLoading ? (
<div className="text-center py-8">Loading system info...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>
<p className="text-sm text-muted-foreground">Node.js Version</p>
<p className="font-medium">{systemInfo.nodeVersion}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Platform</p>
<p className="font-medium">{systemInfo.platform}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Architecture</p>
<p className="font-medium">{systemInfo.architecture}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Uptime</p>
<p className="font-medium">{formatUptime(systemInfo.uptime)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Environment</p>
<p className="font-medium">{systemInfo.env}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Memory Usage</p>
<p className="font-medium">
{formatBytes(systemInfo.memory?.used || 0)} / {formatBytes(systemInfo.memory?.total || 0)}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">CPU Cores</p>
<p className="font-medium">{systemInfo.cpu?.cores || 'N/A'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Load Average</p>
<p className="font-medium">
{systemInfo.cpu?.loadAverage?.map((load: number) => load.toFixed(2)).join(', ') || 'N/A'}
</p>
</div>
</div>
)}
</CardContent>
</Card>
)}
</div>
)
}

View File

@ -1,116 +0,0 @@
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 <div className="text-center py-8">Loading maintenance status...</div>
}
return (
<div className="space-y-6">
<h2 className="text-3xl font-bold">Maintenance Mode</h2>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Maintenance Status</CardTitle>
<Badge variant={status?.enabled ? 'destructive' : 'default'}>
{status?.enabled ? 'Enabled' : 'Disabled'}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Maintenance Mode</Label>
<p className="text-sm text-muted-foreground">
Enable maintenance mode to temporarily disable access to the platform
</p>
</div>
<Switch
checked={status?.enabled || false}
onCheckedChange={handleToggle}
/>
</div>
{!status?.enabled && (
<div className="space-y-2">
<Label htmlFor="message">Maintenance Message (Optional)</Label>
<Input
id="message"
placeholder="Enter maintenance message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<p className="text-sm text-muted-foreground">
This message will be displayed to users when maintenance mode is enabled
</p>
</div>
)}
{status?.enabled && status?.message && (
<div>
<Label>Current Message</Label>
<p className="text-sm mt-2">{status.message}</p>
</div>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,105 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">API Keys</h2>
<Card>
<CardHeader>
<CardTitle>All API Keys</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">Loading API keys...</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>User</TableHead>
<TableHead>Last Used</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiKeys?.map((key: any) => (
<TableRow key={key.id}>
<TableCell className="font-medium">{key.name}</TableCell>
<TableCell>{key.userId || 'N/A'}</TableCell>
<TableCell>
{key.lastUsedAt ? format(new Date(key.lastUsedAt), 'MMM dd, yyyy') : 'Never'}
</TableCell>
<TableCell>
<Badge variant={key.revoked ? 'destructive' : 'default'}>
{key.revoked ? 'Revoked' : 'Active'}
</Badge>
</TableCell>
<TableCell>
{!key.revoked && (
<Button
variant="ghost"
size="icon"
onClick={() => revokeMutation.mutate(key.id)}
>
<Ban className="w-4 h-4" />
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{apiKeys?.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No API keys found
</div>
)}
</>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,105 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">Failed Login Attempts</h2>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Failed Logins</CardTitle>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search by email..."
className="pl-10 w-64"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">Loading failed logins...</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Email</TableHead>
<TableHead>IP Address</TableHead>
<TableHead>User Agent</TableHead>
<TableHead>Reason</TableHead>
<TableHead>Attempted At</TableHead>
<TableHead>Blocked</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{failedLogins?.data?.map((login: any) => (
<TableRow key={login.id}>
<TableCell className="font-medium">{login.email}</TableCell>
<TableCell className="font-mono text-sm">{login.ipAddress}</TableCell>
<TableCell className="max-w-xs truncate">{login.userAgent}</TableCell>
<TableCell>{login.reason || 'N/A'}</TableCell>
<TableCell>
{format(new Date(login.attemptedAt), 'MMM dd, yyyy HH:mm')}
</TableCell>
<TableCell>
<Badge variant={login.blocked ? 'destructive' : 'secondary'}>
{login.blocked ? 'Yes' : 'No'}
</Badge>
</TableCell>
<TableCell>
<Button variant="ghost" size="icon">
<Ban className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{failedLogins?.data?.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No failed login attempts found
</div>
)}
</>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,87 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">Security</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card className="cursor-pointer hover:bg-accent transition-colors" onClick={() => navigate('/admin/security/failed-logins')}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertTriangle className="w-5 h-5" />
Failed Logins
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
View and manage failed login attempts
</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:bg-accent transition-colors" onClick={() => navigate('/admin/security/suspicious')}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="w-5 h-5" />
Suspicious Activity
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Monitor suspicious IPs and emails
</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:bg-accent transition-colors" onClick={() => navigate('/admin/security/api-keys')}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Key className="w-5 h-5" />
API Keys
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Manage API keys and tokens
</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:bg-accent transition-colors" onClick={() => navigate('/admin/security/rate-limits')}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Gauge className="w-5 h-5" />
Rate Limits
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
View rate limit violations
</p>
</CardContent>
</Card>
<Card className="cursor-pointer hover:bg-accent transition-colors" onClick={() => navigate('/admin/security/sessions')}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="w-5 h-5" />
Active Sessions
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Manage active user sessions
</p>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@ -1,67 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">Rate Limit Violations</h2>
<Card>
<CardHeader>
<CardTitle>Recent Violations (Last 7 Days)</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">Loading violations...</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>IP Address</TableHead>
<TableHead>Requests</TableHead>
<TableHead>Period</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{violations?.map((violation: any) => (
<TableRow key={violation.id}>
<TableCell>{violation.userId || 'N/A'}</TableCell>
<TableCell className="font-mono text-sm">{violation.ipAddress}</TableCell>
<TableCell>{violation.requests}</TableCell>
<TableCell>{violation.period}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{violations?.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No rate limit violations found
</div>
)}
</>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,78 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">Active Sessions</h2>
<Card>
<CardHeader>
<CardTitle>All Active Sessions</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">Loading sessions...</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>IP Address</TableHead>
<TableHead>User Agent</TableHead>
<TableHead>Last Activity</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sessions?.map((session: any) => (
<TableRow key={session.id}>
<TableCell className="font-medium">{session.userId || 'N/A'}</TableCell>
<TableCell className="font-mono text-sm">{session.ipAddress}</TableCell>
<TableCell className="max-w-xs truncate">{session.userAgent}</TableCell>
<TableCell>
{format(new Date(session.lastActivity), 'MMM dd, yyyy HH:mm')}
</TableCell>
<TableCell>
<Button variant="ghost" size="icon">
<LogOut className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{sessions?.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No active sessions found
</div>
)}
</>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,91 +0,0 @@
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 (
<div className="space-y-6">
<h2 className="text-3xl font-bold">Suspicious Activity</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="w-5 h-5" />
Suspicious IP Addresses
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">Loading...</div>
) : suspicious?.suspiciousIPs?.length > 0 ? (
<div className="space-y-2">
{suspicious.suspiciousIPs.map((ip: any, index: number) => (
<div key={index} className="flex items-center justify-between p-2 border rounded">
<div>
<p className="font-mono font-medium">{ip.ipAddress}</p>
<p className="text-sm text-muted-foreground">{ip.attempts} attempts</p>
</div>
<Button variant="outline" size="sm">
<Ban className="w-4 h-4 mr-2" />
Block
</Button>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
No suspicious IPs found
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="w-5 h-5" />
Suspicious Emails
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">Loading...</div>
) : suspicious?.suspiciousEmails?.length > 0 ? (
<div className="space-y-2">
{suspicious.suspiciousEmails.map((email: any, index: number) => (
<div key={index} className="flex items-center justify-between p-2 border rounded">
<div>
<p className="font-medium">{email.email}</p>
<p className="text-sm text-muted-foreground">{email.attempts} attempts</p>
</div>
<Button variant="outline" size="sm">
<Ban className="w-4 h-4 mr-2" />
Block
</Button>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
No suspicious emails found
</div>
)}
</CardContent>
</Card>
</div>
</div>
)
}

View File

@ -1,214 +0,0 @@
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 { Switch } from "@/components/ui/switch"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Plus } from "lucide-react"
import { adminApiHelpers } from "@/lib/api-client"
import { toast } from "sonner"
export default function SettingsPage() {
const queryClient = useQueryClient()
const [selectedCategory, setSelectedCategory] = useState<string>("GENERAL")
const [createDialogOpen, setCreateDialogOpen] = useState(false)
const [newSetting, setNewSetting] = useState({
key: "",
value: "",
description: "",
isPublic: false,
})
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 createSettingMutation = useMutation({
mutationFn: async (data: {
key: string
value: string
category: string
description?: string
isPublic?: boolean
}) => {
await adminApiHelpers.createSetting(data)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
toast.success("Setting created successfully")
setCreateDialogOpen(false)
setNewSetting({ key: "", value: "", description: "", isPublic: false })
},
onError: (error: any) => {
toast.error(error.response?.data?.message || "Failed to create setting")
},
})
const handleSave = (key: string, value: string) => {
updateSettingMutation.mutate({ key, value })
}
const handleCreate = () => {
if (!newSetting.key || !newSetting.value) {
toast.error("Key and value are required")
return
}
createSettingMutation.mutate({
key: newSetting.key,
value: newSetting.value,
category: selectedCategory,
description: newSetting.description || undefined,
isPublic: newSetting.isPublic,
})
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold">System Settings</h2>
<Button onClick={() => setCreateDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Create Setting
</Button>
</div>
<Tabs value={selectedCategory} onValueChange={setSelectedCategory}>
<TabsList>
<TabsTrigger value="GENERAL">General</TabsTrigger>
<TabsTrigger value="EMAIL">Email</TabsTrigger>
<TabsTrigger value="STORAGE">Storage</TabsTrigger>
<TabsTrigger value="SECURITY">Security</TabsTrigger>
<TabsTrigger value="API">API</TabsTrigger>
<TabsTrigger value="FEATURES">Features</TabsTrigger>
</TabsList>
<TabsContent value={selectedCategory} className="space-y-4">
<Card>
<CardHeader>
<CardTitle>{selectedCategory} Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{isLoading ? (
<div className="text-center py-8">Loading settings...</div>
) : settings && settings.length > 0 ? (
settings.map((setting: any) => (
<div key={setting.id} className="space-y-2">
<Label htmlFor={setting.key}>{setting.key}</Label>
<div className="flex gap-2">
<Input
id={setting.key}
defaultValue={setting.value}
onBlur={(e) => {
if (e.target.value !== setting.value) {
handleSave(setting.key, e.target.value)
}
}}
/>
</div>
{setting.description && (
<p className="text-sm text-muted-foreground">{setting.description}</p>
)}
</div>
))
) : (
<div className="text-center py-8 text-muted-foreground">
No settings found for this category
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Create Setting Dialog */}
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Setting</DialogTitle>
<DialogDescription>
Create a new system setting in the {selectedCategory} category
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="setting-key">Key *</Label>
<Input
id="setting-key"
placeholder="setting.key"
value={newSetting.key}
onChange={(e) => setNewSetting({ ...newSetting, key: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="setting-value">Value *</Label>
<Input
id="setting-value"
placeholder="Setting value"
value={newSetting.value}
onChange={(e) => setNewSetting({ ...newSetting, value: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="setting-description">Description</Label>
<Input
id="setting-description"
placeholder="Setting description"
value={newSetting.description}
onChange={(e) => setNewSetting({ ...newSetting, description: e.target.value })}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
id="setting-public"
checked={newSetting.isPublic}
onCheckedChange={(checked) => setNewSetting({ ...newSetting, isPublic: checked })}
/>
<Label htmlFor="setting-public" className="text-sm">
Public (accessible via API)
</Label>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => {
setCreateDialogOpen(false)
setNewSetting({ key: "", value: "", description: "", isPublic: false })
}}>
Cancel
</Button>
<Button onClick={handleCreate} disabled={createSettingMutation.isPending}>
{createSettingMutation.isPending ? "Creating..." : "Create"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@ -1,66 +0,0 @@
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 <div className="text-center py-8">Loading activity...</div>
}
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => navigate(`/admin/users/${id}`)}>
<ArrowLeft className="w-4 h-4" />
</Button>
<h2 className="text-3xl font-bold">User Activity</h2>
</div>
<Card>
<CardHeader>
<CardTitle>Activity Timeline (Last 30 Days)</CardTitle>
</CardHeader>
<CardContent>
{activity && activity.length > 0 ? (
<div className="space-y-4">
{activity.map((item: any, index: number) => (
<div key={index} className="border-l-2 pl-4 pb-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">{item.action || 'Activity'}</p>
<p className="text-sm text-muted-foreground">{item.description || item.message}</p>
</div>
<div className="text-sm text-muted-foreground">
{format(new Date(item.createdAt || item.timestamp), 'PPpp')}
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
No activity found
</div>
)}
</CardContent>
</Card>
</div>
)
}

View File

@ -1,155 +0,0 @@
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 <div className="text-center py-8">Loading user details...</div>
}
if (!user) {
return <div className="text-center py-8">User not found</div>
}
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => navigate('/admin/users')}>
<ArrowLeft className="w-4 h-4" />
</Button>
<h2 className="text-3xl font-bold">User Details</h2>
</div>
<Tabs defaultValue="info" className="space-y-4">
<TabsList>
<TabsTrigger value="info">Information</TabsTrigger>
<TabsTrigger value="statistics">Statistics</TabsTrigger>
<TabsTrigger value="activity" onClick={() => navigate(`/admin/users/${id}/activity`)}>
Activity
</TabsTrigger>
</TabsList>
<TabsContent value="info" className="space-y-4">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>User Information</CardTitle>
<div className="flex gap-2">
<Button variant="outline" size="sm">
<Edit className="w-4 h-4 mr-2" />
Edit
</Button>
<Button variant="outline" size="sm">
<Key className="w-4 h-4 mr-2" />
Reset Password
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Email</p>
<p className="font-medium">{user.email}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Name</p>
<p className="font-medium">{user.firstName} {user.lastName}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Role</p>
<Badge>{user.role}</Badge>
</div>
<div>
<p className="text-sm text-muted-foreground">Status</p>
<Badge variant={user.isActive ? 'default' : 'secondary'}>
{user.isActive ? 'Active' : 'Inactive'}
</Badge>
</div>
<div>
<p className="text-sm text-muted-foreground">Created At</p>
<p className="font-medium">{format(new Date(user.createdAt), 'PPpp')}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Updated At</p>
<p className="font-medium">{format(new Date(user.updatedAt), 'PPpp')}</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="statistics" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Invoices</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{user._count?.invoices || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Reports</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{user._count?.reports || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Documents</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{user._count?.documents || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Payments</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{user._count?.payments || 0}</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
)
}

View File

@ -1,406 +0,0 @@
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 { Label } from "@/components/ui/label"
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, Upload } 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<string>("all")
const [statusFilter, setStatusFilter] = useState<string>("all")
const [selectedUser, setSelectedUser] = useState<any>(null)
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false)
const [importDialogOpen, setImportDialogOpen] = useState(false)
const [importFile, setImportFile] = useState<File | null>(null)
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 importUsersMutation = useMutation({
mutationFn: async (file: File) => {
const response = await adminApiHelpers.importUsers(file)
return response.data
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
toast.success(`Imported ${data.success} users. ${data.failed} failed.`)
setImportDialogOpen(false)
setImportFile(null)
if (data.errors && data.errors.length > 0) {
console.error('Import errors:', data.errors)
}
},
onError: (error: any) => {
toast.error(error.response?.data?.message || "Failed to import users")
},
})
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 handleImport = () => {
if (importFile) {
importUsersMutation.mutate(importFile)
}
}
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
setImportFile(file)
}
}
const getRoleBadgeVariant = (role: string) => {
switch (role) {
case 'ADMIN':
return 'destructive'
case 'USER':
return 'default'
case 'VIEWER':
return 'secondary'
default:
return 'outline'
}
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold">Users Management</h2>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setImportDialogOpen(true)}>
<Upload className="w-4 h-4 mr-2" />
Import Users
</Button>
<Button>
<UserPlus className="w-4 h-4 mr-2" />
Add User
</Button>
</div>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>All Users</CardTitle>
<div className="flex items-center gap-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search users..."
className="pl-10 w-64"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<Select value={roleFilter} onValueChange={setRoleFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="All Roles" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Roles</SelectItem>
<SelectItem value="ADMIN">Admin</SelectItem>
<SelectItem value="USER">User</SelectItem>
<SelectItem value="VIEWER">Viewer</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="All Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={handleExport}>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">Loading users...</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Email</TableHead>
<TableHead>Name</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created At</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{usersData?.data?.map((user: any) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.email}</TableCell>
<TableCell>{user.firstName} {user.lastName}</TableCell>
<TableCell>
<Badge variant={getRoleBadgeVariant(user.role)}>
{user.role}
</Badge>
</TableCell>
<TableCell>
<Badge variant={user.isActive ? 'default' : 'secondary'}>
{user.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>
{format(new Date(user.createdAt), 'MMM dd, yyyy')}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => navigate(`/admin/users/${user.id}`)}
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => {
setSelectedUser(user)
setResetPasswordDialogOpen(true)
}}
>
<Key className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => {
setSelectedUser(user)
setDeleteDialogOpen(true)
}}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{usersData?.data?.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No users found
</div>
)}
{usersData && usersData.total > limit && (
<div className="flex items-center justify-between mt-4">
<div className="text-sm text-muted-foreground">
Showing {(page - 1) * limit + 1} to {Math.min(page * limit, usersData.total)} of {usersData.total} users
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setPage(p => p + 1)}
disabled={page * limit >= usersData.total}
>
Next
</Button>
</div>
</div>
)}
</>
)}
</CardContent>
</Card>
{/* Delete Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete User</DialogTitle>
<DialogDescription>
Are you sure you want to delete {selectedUser?.email}? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
Cancel
</Button>
<Button variant="destructive" onClick={handleDelete}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Reset Password Dialog */}
<Dialog open={resetPasswordDialogOpen} onOpenChange={setResetPasswordDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Reset Password</DialogTitle>
<DialogDescription>
Reset password for {selectedUser?.email}? A temporary password will be generated.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setResetPasswordDialogOpen(false)}>
Cancel
</Button>
<Button onClick={handleResetPassword}>
Reset Password
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Import Users Dialog */}
<Dialog open={importDialogOpen} onOpenChange={setImportDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Import Users</DialogTitle>
<DialogDescription>
Upload a CSV file with user data. The file should contain columns: email, firstName, lastName, role (optional).
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="import-file">CSV File</Label>
<Input
id="import-file"
type="file"
accept=".csv"
onChange={handleFileChange}
/>
{importFile && (
<p className="text-sm text-muted-foreground">
Selected: {importFile.name}
</p>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => {
setImportDialogOpen(false)
setImportFile(null)
}}>
Cancel
</Button>
<Button onClick={handleImport} disabled={!importFile || importUsersMutation.isPending}>
{importUsersMutation.isPending ? "Importing..." : "Import"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}