Admin
This commit is contained in:
parent
f80ff9317e
commit
20b0251259
99
src/components/ui/date-range-picker.tsx
Normal file
99
src/components/ui/date-range-picker.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -32,6 +32,35 @@ adminApi.interceptors.response.use(
|
||||||
// Redirect to login
|
// Redirect to login
|
||||||
localStorage.removeItem('access_token');
|
localStorage.removeItem('access_token');
|
||||||
window.location.href = '/login';
|
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);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = () => {
|
const handleExport = () => {
|
||||||
toast.success("Exporting dashboard data...")
|
toast.success("Exporting dashboard data...")
|
||||||
}
|
}
|
||||||
|
|
@ -215,6 +223,50 @@ export default function DashboardPage() {
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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 */}
|
{/* System Health */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,29 @@ import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
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 { adminApiHelpers } from "@/lib/api-client"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>("GENERAL")
|
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({
|
const { data: settings, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'settings', selectedCategory],
|
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) => {
|
const handleSave = (key: string, value: string) => {
|
||||||
updateSettingMutation.mutate({ key, value })
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-3xl font-bold">System Settings</h2>
|
<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}>
|
<Tabs value={selectedCategory} onValueChange={setSelectedCategory}>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
|
|
@ -88,6 +146,68 @@ export default function SettingsPage() {
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
@ -28,7 +29,7 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} 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 { adminApiHelpers } from "@/lib/api-client"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
|
|
@ -44,6 +45,8 @@ export default function UsersPage() {
|
||||||
const [selectedUser, setSelectedUser] = useState<any>(null)
|
const [selectedUser, setSelectedUser] = useState<any>(null)
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false)
|
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false)
|
||||||
|
const [importDialogOpen, setImportDialogOpen] = useState(false)
|
||||||
|
const [importFile, setImportFile] = useState<File | null>(null)
|
||||||
|
|
||||||
const { data: usersData, isLoading } = useQuery({
|
const { data: usersData, isLoading } = useQuery({
|
||||||
queryKey: ['admin', 'users', page, limit, search, roleFilter, statusFilter],
|
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 () => {
|
const handleExport = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await adminApiHelpers.exportUsers('csv')
|
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<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
setImportFile(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getRoleBadgeVariant = (role: string) => {
|
const getRoleBadgeVariant = (role: string) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case 'ADMIN':
|
case 'ADMIN':
|
||||||
|
|
@ -129,10 +164,16 @@ export default function UsersPage() {
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-3xl font-bold">Users Management</h2>
|
<h2 className="text-3xl font-bold">Users Management</h2>
|
||||||
<Button>
|
<div className="flex gap-2">
|
||||||
<UserPlus className="w-4 h-4 mr-2" />
|
<Button variant="outline" onClick={() => setImportDialogOpen(true)}>
|
||||||
Add User
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
</Button>
|
Import Users
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<UserPlus className="w-4 h-4 mr-2" />
|
||||||
|
Add User
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
|
@ -320,6 +361,45 @@ export default function UsersPage() {
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user