Add comprehensive admin features and multi-system integration
- Add admin dashboard with sync controls and data browser - Implement RMM, Auvik, and Addigy organization mappings - Add chunked ticket sync with progress tracking - Implement entity sync service with rate limiting - Add analytics engine and performance optimizer - Create data browser for all PSA entities - Add navigation components and UI improvements - Implement background processing and sync services - Add comprehensive documentation and migration scripts - Update configuration items with multi-system support - Enhance contact management and purchase history - Add issue type assignment and LLM analyzer - Improve error handling and logging utilities
This commit is contained in:
parent
e8462ef301
commit
6eee14f8af
171 changed files with 32671 additions and 621 deletions
212
components/admin/DataTable.tsx
Normal file
212
components/admin/DataTable.tsx
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Search, ArrowUpDown, ArrowUp, ArrowDown, Loader2 } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
interface Column {
|
||||
key: string;
|
||||
label: string;
|
||||
sortable?: boolean;
|
||||
render?: (value: any, row: any) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface DataTableProps {
|
||||
columns: Column[];
|
||||
data: any[];
|
||||
totalCount: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onSort?: (column: string, direction: 'asc' | 'desc') => void;
|
||||
onSearch?: (query: string) => void;
|
||||
onRowClick?: (row: any) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export default function DataTable({
|
||||
columns,
|
||||
data,
|
||||
totalCount,
|
||||
page,
|
||||
pageSize,
|
||||
onPageChange,
|
||||
onSort,
|
||||
onSearch,
|
||||
onRowClick,
|
||||
isLoading = false,
|
||||
}: DataTableProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
const handleSort = (columnKey: string) => {
|
||||
if (!onSort) return;
|
||||
|
||||
const newDirection = sortColumn === columnKey && sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
setSortColumn(columnKey);
|
||||
setSortDirection(newDirection);
|
||||
onSort(columnKey, newDirection);
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
if (onSearch) {
|
||||
onSearch(searchQuery);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Search Bar */}
|
||||
{onSearch && (
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleSearch} disabled={isLoading}>
|
||||
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />}
|
||||
<span className="ml-2">Search</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table */}
|
||||
<div className="border rounded-lg overflow-hidden bg-card">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50 hover:bg-muted/50">
|
||||
{columns.map((column) => (
|
||||
<TableHead key={column.key} className="font-semibold">
|
||||
{column.sortable ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleSort(column.key)}
|
||||
className="h-8 -ml-3 hover:bg-muted/80 transition-colors"
|
||||
>
|
||||
{column.label}
|
||||
{sortColumn === column.key ? (
|
||||
sortDirection === 'asc' ? (
|
||||
<ArrowUp className="ml-2 h-4 w-4" />
|
||||
) : (
|
||||
<ArrowDown className="ml-2 h-4 w-4" />
|
||||
)
|
||||
) : (
|
||||
<ArrowUpDown className="ml-2 h-4 w-4 opacity-50" />
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
column.label
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
Array.from({ length: 5 }).map((_, index) => (
|
||||
<TableRow key={index}>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.key}>
|
||||
<Skeleton className="h-5 w-full" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : data.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="text-center py-12">
|
||||
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
||||
<Search className="w-8 h-8 opacity-50" />
|
||||
<p className="text-sm font-medium">No data found</p>
|
||||
<p className="text-xs">Try adjusting your search or filters</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
<TableRow
|
||||
key={row.id || index}
|
||||
className={onRowClick ? 'cursor-pointer hover:bg-muted/50 transition-colors' : ''}
|
||||
onClick={() => onRowClick?.(row)}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.key}>
|
||||
{column.render ? column.render(row[column.key], row) : row[column.key]}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 px-2">
|
||||
<div className="text-sm text-muted-foreground font-medium">
|
||||
Showing <span className="font-semibold text-foreground">{Math.min((page - 1) * pageSize + 1, totalCount)}</span> to{' '}
|
||||
<span className="font-semibold text-foreground">{Math.min(page * pageSize, totalCount)}</span> of{' '}
|
||||
<span className="font-semibold text-foreground">{totalCount}</span> results
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => onPageChange(1)}
|
||||
disabled={page === 1 || isLoading}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<ChevronsLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
disabled={page === 1 || isLoading}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-1 px-3">
|
||||
<span className="text-sm font-medium">
|
||||
Page {page} of {totalPages || 1}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={page === totalPages || isLoading}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
disabled={page === totalPages || isLoading}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<ChevronsRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue