2025-08-14 12:16:32 +07:00
|
|
|
import { createFileRoute, useParams } from "@tanstack/react-router";
|
|
|
|
|
import { useQueryData } from "@/hooks/useQueryData";
|
|
|
|
|
import { API_ENDPOINTS, BASE_URL } from "@/config/api";
|
|
|
|
|
import {
|
|
|
|
|
flexRender,
|
|
|
|
|
getCoreRowModel,
|
2025-09-26 17:56:55 +07:00
|
|
|
getPaginationRowModel,
|
2025-08-14 12:16:32 +07:00
|
|
|
useReactTable,
|
|
|
|
|
type ColumnDef,
|
|
|
|
|
} from "@tanstack/react-table";
|
|
|
|
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableRow,
|
|
|
|
|
} from "@/components/ui/table";
|
|
|
|
|
import { useDeviceEvents } from "@/hooks/useDeviceEvents";
|
2025-09-26 17:56:55 +07:00
|
|
|
import {
|
|
|
|
|
ChevronLeft,
|
|
|
|
|
ChevronRight,
|
|
|
|
|
Clock,
|
|
|
|
|
Loader2,
|
|
|
|
|
MapPin,
|
|
|
|
|
Monitor,
|
|
|
|
|
Wifi,
|
|
|
|
|
WifiOff,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2025-08-11 23:21:36 +07:00
|
|
|
|
2025-08-14 12:16:32 +07:00
|
|
|
export const Route = createFileRoute("/_authenticated/room/$roomName/")({
|
|
|
|
|
head: ({ params }) => ({
|
|
|
|
|
meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }],
|
|
|
|
|
}),
|
|
|
|
|
component: RoomDetailComponent,
|
|
|
|
|
});
|
2025-08-11 23:21:36 +07:00
|
|
|
|
2025-08-14 12:16:32 +07:00
|
|
|
function RoomDetailComponent() {
|
|
|
|
|
const { roomName } = useParams({ from: "/_authenticated/room/$roomName/" });
|
|
|
|
|
|
|
|
|
|
const { data: devices = [], isLoading } = useQueryData({
|
|
|
|
|
queryKey: ["devices", roomName],
|
|
|
|
|
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.GET_DEVICE_FROM_ROOM(roomName),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Lắng nghe SSE và update state
|
2025-08-27 22:36:27 +07:00
|
|
|
useDeviceEvents(roomName);
|
2025-08-14 12:16:32 +07:00
|
|
|
|
|
|
|
|
const columns: ColumnDef<any>[] = [
|
|
|
|
|
{
|
|
|
|
|
header: "STT",
|
2025-09-26 17:56:55 +07:00
|
|
|
cell: ({ row }) => (
|
|
|
|
|
<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">{row.index + 1}</span>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
2025-08-14 12:16:32 +07:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-26 17:56:55 +07:00
|
|
|
header: () => (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Clock className="h-4 w-4" />
|
|
|
|
|
Thời gian thiết bị
|
|
|
|
|
</div>
|
|
|
|
|
),
|
2025-08-14 12:16:32 +07:00
|
|
|
accessorKey: "deviceTime",
|
|
|
|
|
cell: ({ getValue }) => {
|
|
|
|
|
const date = new Date(getValue() as string);
|
2025-09-26 17:56:55 +07:00
|
|
|
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>
|
|
|
|
|
);
|
2025-08-14 12:16:32 +07:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
header: "Phiên bản",
|
|
|
|
|
accessorKey: "version",
|
2025-09-26 17:56:55 +07:00
|
|
|
cell: ({ getValue }) => (
|
|
|
|
|
<Badge variant="secondary" className="font-mono">
|
|
|
|
|
v{getValue() as string}
|
|
|
|
|
</Badge>
|
|
|
|
|
),
|
2025-08-14 12:16:32 +07:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-26 17:56:55 +07:00
|
|
|
header: () => (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<MapPin className="h-4 w-4" />
|
2025-10-06 15:52:48 +07:00
|
|
|
Phòng
|
2025-09-26 17:56:55 +07:00
|
|
|
</div>
|
|
|
|
|
),
|
2025-10-06 15:52:48 +07:00
|
|
|
accessorKey: "room",
|
2025-09-26 17:56:55 +07:00
|
|
|
cell: ({ getValue }) => (
|
2025-10-06 15:52:48 +07:00
|
|
|
<span className="text-sm font-medium">{getValue() as string}</span>
|
2025-09-26 17:56:55 +07:00
|
|
|
),
|
2025-08-14 12:16:32 +07:00
|
|
|
},
|
2025-10-06 15:52:48 +07:00
|
|
|
{
|
|
|
|
|
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 networkInfos = getValue() as {
|
|
|
|
|
macAddress?: string;
|
|
|
|
|
ipAddress?: string;
|
|
|
|
|
}[];
|
|
|
|
|
|
|
|
|
|
if (!networkInfos || networkInfos.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">
|
|
|
|
|
{networkInfos.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>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-08-14 12:16:32 +07:00
|
|
|
{
|
|
|
|
|
header: "Trạng thái",
|
|
|
|
|
accessorKey: "isOffline",
|
2025-09-26 17:56:55 +07:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
},
|
2025-08-14 12:16:32 +07:00
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const table = useReactTable({
|
|
|
|
|
data: devices,
|
|
|
|
|
columns,
|
|
|
|
|
getCoreRowModel: getCoreRowModel(),
|
2025-09-26 17:56:55 +07:00
|
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
|
|
|
initialState: { pagination: { pageSize: 16 } },
|
2025-08-14 12:16:32 +07:00
|
|
|
});
|
|
|
|
|
|
2025-09-26 17:56:55 +07:00
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="w-full px-6 py-8">
|
|
|
|
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
|
|
|
<div className="flex flex-col items-center gap-4">
|
|
|
|
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
|
|
|
<p className="text-muted-foreground">Đang tải danh sách phòng...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onlineDevices = devices.filter(
|
|
|
|
|
(device: any) => !device.isOffline
|
|
|
|
|
).length;
|
|
|
|
|
const offlineDevices = devices.length - onlineDevices;
|
2025-08-14 12:16:32 +07:00
|
|
|
|
|
|
|
|
return (
|
2025-09-26 17:56:55 +07:00
|
|
|
<div className="w-full px-6 space-y-6">
|
2025-08-14 12:16:32 +07:00
|
|
|
<div className="flex items-center justify-between">
|
2025-09-26 17:56:55 +07:00
|
|
|
<div className="space-y-1">
|
|
|
|
|
<h1 className="text-3xl font-bold tracking-tight">
|
|
|
|
|
Phòng: {roomName}
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
Quản lý và theo dõi thiết bị trong phòng
|
2025-08-14 12:16:32 +07:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2025-09-26 17:56:55 +07:00
|
|
|
<div className="flex gap-4">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<div className="text-2xl font-bold text-green-600">
|
|
|
|
|
{onlineDevices}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm text-muted-foreground">Online</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<div className="text-2xl font-bold text-red-600">
|
|
|
|
|
{offlineDevices}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm text-muted-foreground">Offline</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<div className="text-2xl font-bold">{devices.length}</div>
|
|
|
|
|
<div className="text-sm text-muted-foreground">Tổng cộng</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-08-14 12:16:32 +07:00
|
|
|
</div>
|
2025-09-26 17:56:55 +07:00
|
|
|
|
|
|
|
|
<Card className="shadow-sm">
|
|
|
|
|
<CardHeader className="bg-muted/50">
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
|
<Monitor className="h-5 w-5" />
|
|
|
|
|
Danh sách thiết bị
|
|
|
|
|
</CardTitle>
|
2025-08-14 12:16:32 +07:00
|
|
|
</CardHeader>
|
2025-09-26 17:56:55 +07:00
|
|
|
<CardContent className="p-0">
|
|
|
|
|
{devices.length === 0 ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center py-12">
|
|
|
|
|
<Monitor className="h-12 w-12 text-muted-foreground mb-4" />
|
|
|
|
|
<h3 className="text-lg font-semibold mb-2">Không có thiết bị</h3>
|
|
|
|
|
<p className="text-muted-foreground text-center max-w-sm">
|
|
|
|
|
Phòng này chưa có thiết bị nào được kết nối.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2025-08-14 12:16:32 +07:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-08-11 23:21:36 +07:00
|
|
|
}
|