wulf-pulse/components/admin/DataTable.tsx
root 6eee14f8af 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
2025-11-19 14:18:16 -05:00

212 lines
7.3 KiB
TypeScript

'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>
);
}