diff --git a/src/components/ui/date-range-picker.tsx b/src/components/ui/date-range-picker.tsx new file mode 100644 index 0000000..153acae --- /dev/null +++ b/src/components/ui/date-range-picker.tsx @@ -0,0 +1,99 @@ +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 ( + + + + + + + Select Date Range + + Choose a start and end date to filter logs + + +
+
+ + setLocalStartDate(e.target.value)} + /> +
+
+ + setLocalEndDate(e.target.value)} + /> +
+
+ + +
+
+
+
+ ) +} + diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 90f0a80..25571fc 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -32,6 +32,35 @@ adminApi.interceptors.response.use( // 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); } diff --git a/src/pages/admin/dashboard/index.tsx b/src/pages/admin/dashboard/index.tsx index c222cd5..5f4a2ee 100644 --- a/src/pages/admin/dashboard/index.tsx +++ b/src/pages/admin/dashboard/index.tsx @@ -39,6 +39,14 @@ export default function DashboardPage() { }, }) + 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...") } @@ -215,6 +223,50 @@ export default function DashboardPage() { + {/* Error Rate Chart */} + {errorRate && ( + + + + + Error Rate (Last 7 Days) + + + + {errorRateLoading ? ( +
Loading...
+ ) : ( +
+
+
+

Total Errors

+

{errorRate.errors || 0}

+
+
+

Total Requests

+

{errorRate.total || 0}

+
+
+

Error Rate

+

+ {errorRate.errorRate ? `${errorRate.errorRate.toFixed(2)}%` : '0%'} +

+
+
+
+
+
+
+ )} + + + )} + {/* System Health */} diff --git a/src/pages/admin/settings/index.tsx b/src/pages/admin/settings/index.tsx index f805dd5..8a2a7c9 100644 --- a/src/pages/admin/settings/index.tsx +++ b/src/pages/admin/settings/index.tsx @@ -5,12 +5,29 @@ 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("GENERAL") + const [createDialogOpen, setCreateDialogOpen] = useState(false) + const [newSetting, setNewSetting] = useState({ + key: "", + value: "", + description: "", + isPublic: false, + }) const { data: settings, isLoading } = useQuery({ queryKey: ['admin', 'settings', selectedCategory], @@ -33,13 +50,54 @@ export default function SettingsPage() { }, }) + 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 (
-

System Settings

+
+

System Settings

+ +
@@ -88,6 +146,68 @@ export default function SettingsPage() { + + {/* Create Setting Dialog */} + + + + Create New Setting + + Create a new system setting in the {selectedCategory} category + + +
+
+ + setNewSetting({ ...newSetting, key: e.target.value })} + /> +
+
+ + setNewSetting({ ...newSetting, value: e.target.value })} + /> +
+
+ + setNewSetting({ ...newSetting, description: e.target.value })} + /> +
+
+ setNewSetting({ ...newSetting, isPublic: checked })} + /> + +
+
+ + + + +
+
) } diff --git a/src/pages/admin/users/index.tsx b/src/pages/admin/users/index.tsx index 4268f65..b69a564 100644 --- a/src/pages/admin/users/index.tsx +++ b/src/pages/admin/users/index.tsx @@ -5,6 +5,7 @@ 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, @@ -28,7 +29,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" -import { Search, Download, Eye, MoreVertical, UserPlus, Edit, Trash2, Key } from "lucide-react" +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" @@ -44,6 +45,8 @@ export default function UsersPage() { const [selectedUser, setSelectedUser] = useState(null) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false) + const [importDialogOpen, setImportDialogOpen] = useState(false) + const [importFile, setImportFile] = useState(null) const { data: usersData, isLoading } = useQuery({ queryKey: ['admin', 'users', page, limit, search, roleFilter, statusFilter], @@ -85,6 +88,25 @@ export default function UsersPage() { }, }) + 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') @@ -112,6 +134,19 @@ export default function UsersPage() { } } + const handleImport = () => { + if (importFile) { + importUsersMutation.mutate(importFile) + } + } + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) { + setImportFile(file) + } + } + const getRoleBadgeVariant = (role: string) => { switch (role) { case 'ADMIN': @@ -129,10 +164,16 @@ export default function UsersPage() {

Users Management

- +
+ + +
@@ -320,6 +361,45 @@ export default function UsersPage() { + + {/* Import Users Dialog */} + + + + Import Users + + Upload a CSV file with user data. The file should contain columns: email, firstName, lastName, role (optional). + + +
+
+ + + {importFile && ( +

+ Selected: {importFile.name} +

+ )} +
+
+ + + + +
+
) }