226 lines
7.1 KiB
TypeScript
226 lines
7.1 KiB
TypeScript
import {
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getPaginationRowModel,
|
|
useReactTable,
|
|
type ColumnDef,
|
|
} from "@tanstack/react-table";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Wifi, WifiOff, Clock, MapPin, Monitor, ChevronLeft, ChevronRight } from "lucide-react";
|
|
import { useMachineNumber } from "@/hooks/useMachineNumber";
|
|
|
|
interface DeviceTableProps {
|
|
devices: any[];
|
|
}
|
|
|
|
/**
|
|
* Component hiển thị danh sách thiết bị ở dạng bảng
|
|
*/
|
|
export function DeviceTable({ devices }: DeviceTableProps) {
|
|
const getMachineNumber = useMachineNumber();
|
|
|
|
const columns: ColumnDef<any>[] = [
|
|
{
|
|
header: "STT",
|
|
cell: ({ row }) => {
|
|
const machineNumber = getMachineNumber(row.original.id || "");
|
|
return (
|
|
<div className="flex items-center gap-3">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarFallback className="bg-primary/10 text-primary">
|
|
<Monitor className="h-4 w-4" />
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span className="font-medium text-sm">{machineNumber}</span>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
header: () => (
|
|
<div className="flex items-center gap-2">
|
|
<Clock className="h-4 w-4" />
|
|
Thời gian thiết bị
|
|
</div>
|
|
),
|
|
accessorKey: "deviceTime",
|
|
cell: ({ getValue }) => {
|
|
const date = new Date(getValue() as string);
|
|
return (
|
|
<div className="text-sm">
|
|
<div className="font-medium">{date.toLocaleDateString("vi-VN")}</div>
|
|
<div className="text-muted-foreground">{date.toLocaleTimeString("vi-VN")}</div>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
header: "Phiên bản",
|
|
accessorKey: "version",
|
|
cell: ({ getValue }) => (
|
|
<Badge variant="secondary" className="font-mono">
|
|
v{getValue() as string}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
header: () => (
|
|
<div className="flex items-center gap-2">
|
|
<MapPin className="h-4 w-4" />
|
|
Phòng
|
|
</div>
|
|
),
|
|
accessorKey: "room",
|
|
cell: ({ getValue }) => <span className="text-sm font-medium">{getValue() as string}</span>,
|
|
},
|
|
{
|
|
header: () => (
|
|
<div className="flex items-center gap-2">
|
|
<Wifi className="h-4 w-4" />
|
|
Thông tin mạng
|
|
</div>
|
|
),
|
|
accessorKey: "networkInfos",
|
|
cell: ({ getValue }) => {
|
|
const infos = getValue() as { macAddress?: string; ipAddress?: string }[];
|
|
if (!infos || infos.length === 0) {
|
|
return <span className="text-muted-foreground text-sm">Không có dữ liệu</span>;
|
|
}
|
|
return (
|
|
<div className="flex flex-col gap-1">
|
|
{infos.map((info, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="flex items-center gap-2 text-sm font-mono px-2 py-1 rounded bg-muted/30"
|
|
>
|
|
<span className="text-primary">•</span>
|
|
<code className="bg-background px-2 py-0.5 rounded">
|
|
{info.macAddress ?? "-"}
|
|
</code>
|
|
<span className="text-muted-foreground">→</span>
|
|
<code className="bg-background px-2 py-0.5 rounded">
|
|
{info.ipAddress ?? "-"}
|
|
</code>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
header: "Trạng thái",
|
|
accessorKey: "isOffline",
|
|
cell: ({ getValue }) => {
|
|
const isOffline = getValue() as boolean;
|
|
return (
|
|
<Badge
|
|
variant={isOffline ? "destructive" : "default"}
|
|
className={`flex items-center gap-1 w-fit ${
|
|
isOffline
|
|
? "bg-red-100 text-red-700 hover:bg-red-100"
|
|
: "bg-green-100 text-green-700 hover:bg-green-100"
|
|
}`}
|
|
>
|
|
{isOffline ? <WifiOff className="h-3 w-3" /> : <Wifi className="h-3 w-3" />}
|
|
{isOffline ? "Offline" : "Online"}
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
const table = useReactTable({
|
|
data: devices,
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
initialState: { pagination: { pageSize: 16 } },
|
|
});
|
|
|
|
return (
|
|
<div className="max-h-[600px] overflow-y-auto">
|
|
<Table>
|
|
<TableHeader className="sticky top-0 bg-background z-10">
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id} className="hover:bg-transparent border-b">
|
|
{headerGroup.headers.map((header) => (
|
|
<TableHead key={header.id} className="font-semibold text-foreground bg-muted/30">
|
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
</TableHead>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{table.getRowModel().rows.map((row) => (
|
|
<TableRow key={row.id} className="hover:bg-muted/50 transition-colors">
|
|
{row.getVisibleCells().map((cell) => (
|
|
<TableCell key={cell.id} className="py-4">
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
|
|
{/* Pagination */}
|
|
<div className="flex items-center justify-between p-4 border-t bg-muted/20">
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<span>
|
|
Hiển thị{" "}
|
|
{table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} -{" "}
|
|
{Math.min(
|
|
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
|
|
devices.length
|
|
)}{" "}
|
|
trong tổng số {devices.length} thiết bị
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.previousPage()}
|
|
disabled={!table.getCanPreviousPage()}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
Trước
|
|
</Button>
|
|
|
|
<div className="flex items-center gap-1 text-sm font-medium">
|
|
<span>Trang</span>
|
|
<span className="bg-primary text-primary-foreground px-2 py-1 rounded">
|
|
{table.getState().pagination.pageIndex + 1}
|
|
</span>
|
|
<span>của {table.getPageCount()}</span>
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.nextPage()}
|
|
disabled={!table.getCanNextPage()}
|
|
className="flex items-center gap-1"
|
|
>
|
|
Sau
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|