From 00be81e18bc6609a839640563f8d7d16d705775e Mon Sep 17 00:00:00 2001 From: phuongdm Date: Thu, 14 Aug 2025 12:16:32 +0700 Subject: [PATCH] add devices list table --- package-lock.json | 32 ++++ package.json | 1 + src/config/api.ts | 4 + src/hooks/useDeviceEvents.ts | 55 ++++++ .../_authenticated/room/$roomName/index.tsx | 161 +++++++++++++++++- src/routes/_authenticated/room/index.tsx | 160 ++++++++++++++++- 6 files changed, 403 insertions(+), 10 deletions(-) create mode 100644 src/hooks/useDeviceEvents.ts diff --git a/package-lock.json b/package-lock.json index fc46afc..a6257f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@tanstack/react-query": "^5.83.0", "@tanstack/react-router": "^1.121.2", "@tanstack/react-router-devtools": "^1.121.2", + "@tanstack/react-table": "^8.21.3", "@tanstack/router-plugin": "^1.121.2", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", @@ -2496,6 +2497,25 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tanstack/router-core": { "version": "1.129.8", "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.129.8.tgz", @@ -2648,6 +2668,18 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/virtual-file-routes": { "version": "1.129.7", "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.129.7.tgz", diff --git a/package.json b/package.json index 3b9c9fa..3515800 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@tanstack/react-query": "^5.83.0", "@tanstack/react-router": "^1.121.2", "@tanstack/react-router-devtools": "^1.121.2", + "@tanstack/react-table": "^8.21.3", "@tanstack/router-plugin": "^1.121.2", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", diff --git a/src/config/api.ts b/src/config/api.ts index 4b96e77..a3a9ba5 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -28,4 +28,8 @@ export const API_ENDPOINTS = { GET_ROOM_LIST: `/DeviceComm/rooms`, GET_DEVICE_FROM_ROOM: (roomName: string) => `/DeviceComm/room/${roomName}`, }, + SSE_EVENTS: { + DEVICE_ONLINE: `/Sse/events/onlineDevices`, + DEVICE_OFFLINE: `/Sse/events/offlineDevices`, + }, }; diff --git a/src/hooks/useDeviceEvents.ts b/src/hooks/useDeviceEvents.ts new file mode 100644 index 0000000..a0cdcfd --- /dev/null +++ b/src/hooks/useDeviceEvents.ts @@ -0,0 +1,55 @@ +import { useEffect } from "react"; +import { BASE_URL, API_ENDPOINTS } from "@/config/api"; +interface DeviceEventData { + Message: string; + DeviceId: string; + Room: string; + Timestamp?: string; +} + +interface UseDeviceEventsOptions { + onRoomDeviceOnline?: (room: string, deviceId: string) => void; + onRoomDeviceOffline?: (room: string, deviceId: string) => void; + onDeviceOnlineInRoom?: (deviceId: string, room: string) => void; + onDeviceOfflineInRoom?: (deviceId: string, room: string) => void; +} + +export function useDeviceEvents(options: UseDeviceEventsOptions) { + useEffect(() => { + const onlineES = new EventSource(`${BASE_URL}${API_ENDPOINTS.SSE_EVENTS.DEVICE_ONLINE}`); + const offlineES = new EventSource(`${BASE_URL}${API_ENDPOINTS.SSE_EVENTS.DEVICE_OFFLINE}`); + + onlineES.addEventListener("online", (event) => { + try { + const data: DeviceEventData = JSON.parse(event.data); + options.onRoomDeviceOnline?.(data.Room, data.DeviceId); + options.onDeviceOnlineInRoom?.(data.DeviceId, data.Room); + } catch (err) { + console.error("Error parsing online event:", err); + } + }); + + offlineES.addEventListener("offline", (event) => { + try { + const data: DeviceEventData = JSON.parse(event.data); + options.onRoomDeviceOffline?.(data.Room, data.DeviceId); + options.onDeviceOfflineInRoom?.(data.DeviceId, data.Room); + } catch (err) { + console.error("Error parsing offline event:", err); + } + }); + + onlineES.onerror = (err) => { + console.error("Online SSE connection error:", err); + }; + + offlineES.onerror = (err) => { + console.error("Offline SSE connection error:", err); + }; + + return () => { + onlineES.close(); + offlineES.close(); + }; + }, [options]); +} diff --git a/src/routes/_authenticated/room/$roomName/index.tsx b/src/routes/_authenticated/room/$roomName/index.tsx index 653334b..8a7361d 100644 --- a/src/routes/_authenticated/room/$roomName/index.tsx +++ b/src/routes/_authenticated/room/$roomName/index.tsx @@ -1,9 +1,158 @@ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute, useParams } from "@tanstack/react-router"; +import { useQueryData } from "@/hooks/useQueryData"; +import { API_ENDPOINTS, BASE_URL } from "@/config/api"; +import { + flexRender, + getCoreRowModel, + 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 { useQueryClient } from "@tanstack/react-query"; +import { useDeviceEvents } from "@/hooks/useDeviceEvents"; -export const Route = createFileRoute('/_authenticated/room/$roomName/')({ - component: RouteComponent, -}) +export const Route = createFileRoute("/_authenticated/room/$roomName/")({ + head: ({ params }) => ({ + meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }], + }), + component: RoomDetailComponent, +}); -function RouteComponent() { - return
Hello "/_authenticated/room/$roomName/"!
+function RoomDetailComponent() { + const { roomName } = useParams({ from: "/_authenticated/room/$roomName/" }); + const queryClient = useQueryClient(); + + 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 + useDeviceEvents({ + onDeviceOnlineInRoom: (deviceId, room) => { + if (room === roomName) { + queryClient.setQueryData( + ["devices", roomName], + (oldDevices: any[] = []) => + oldDevices.map((d) => + d.macAddress === deviceId ? { ...d, isOffline: false } : d + ) + ); + } + }, + onDeviceOfflineInRoom: (deviceId, room) => { + if (room === roomName) { + queryClient.setQueryData( + ["devices", roomName], + (oldDevices: any[] = []) => + oldDevices.map((d) => + d.macAddress === deviceId ? { ...d, isOffline: true } : d + ) + ); + } + }, + }); + + const columns: ColumnDef[] = [ + { + header: "STT", + cell: ({ row }) => row.index + 1, + }, + { + header: "MAC Address", + accessorKey: "macAddress", + }, + { + header: "Thời gian thiết bị", + accessorKey: "deviceTime", + cell: ({ getValue }) => { + const date = new Date(getValue() as string); + return date.toLocaleString(); + }, + }, + { + header: "Phiên bản", + accessorKey: "version", + }, + { + header: "Địa chỉ IP", + accessorKey: "ipAddress", + }, + { + header: "Trạng thái", + accessorKey: "isOffline", + cell: ({ getValue }) => + getValue() ? ( + Offline + ) : ( + Online + ), + }, + ]; + + const table = useReactTable({ + data: devices, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + if (isLoading) return
Đang tải thiết bị...
; + + return ( +
+
+
+

Phòng: {roomName}

+

+ Danh sách thiết bị trong phòng +

+
+
+ + + Thiết bị + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+
+
+
+ ); } diff --git a/src/routes/_authenticated/room/index.tsx b/src/routes/_authenticated/room/index.tsx index 4c32779..df53889 100644 --- a/src/routes/_authenticated/room/index.tsx +++ b/src/routes/_authenticated/room/index.tsx @@ -1,9 +1,161 @@ -import { createFileRoute } from '@tanstack/react-router' +import { API_ENDPOINTS, BASE_URL } from "@/config/api"; +import { useQueryData } from "@/hooks/useQueryData"; +import { useDeviceEvents } from "@/hooks/useDeviceEvents"; +import { useQueryClient } from "@tanstack/react-query"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable, + type ColumnDef, + type SortingState, +} 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 React from "react"; -export const Route = createFileRoute('/_authenticated/room/')({ +export const Route = createFileRoute("/_authenticated/room/")({ + head: () => ({ + meta: [{ title: "Danh sách phòng" }], + }), component: RoomComponent, -}) +}); function RoomComponent() { - return
Hello "/_authenticated/room/"!
+ const queryClient = useQueryClient(); + const navigate = useNavigate(); + + const { data: roomData = [], isLoading } = useQueryData({ + queryKey: ["rooms"], + url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST, + }); + + const [sorting, setSorting] = React.useState([]); + + useDeviceEvents({ + onRoomDeviceOnline: (room) => { + queryClient.setQueryData(["rooms"], (oldRooms: any[] = []) => + oldRooms.map((r) => + r.name === room + ? { ...r, offlineCount: Math.max((r.offlineCount || 0) - 1, 0) } + : r + ) + ); + }, + onRoomDeviceOffline: (room) => { + queryClient.setQueryData(["rooms"], (oldRooms: any[] = []) => + oldRooms.map((r) => + r.name === room + ? { ...r, offlineCount: (r.offlineCount || 0) + 1 } + : r + ) + ); + }, + }); + + const columns: ColumnDef[] = [ + { + header: "STT", + cell: ({ row }) => row.index + 1, + }, + { + header: "Tên phòng", + accessorKey: "name", + }, + { + header: "Số lượng thiết bị", + accessorKey: "numberOfDevices", + }, + { + header: "Thiết bị offline", + accessorKey: "offlineCount", + cell: ({ getValue }) => getValue() || 0, + }, + ]; + + const table = useReactTable({ + data: roomData, + columns, + state: { sorting }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + + if (isLoading) return
Đang tải...
; + + return ( +
+
+
+

Quản lý phòng

+

+ Danh sách các phòng hiện có trong hệ thống +

+
+
+ + + Danh sách phòng + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isSorted = header.column.getIsSorted(); + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {isSorted ? (isSorted === "asc" ? " ▲" : " ▼") : ""} + + ); + })} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + navigate({ + to: "/room/$roomName", + params: { roomName: row.original.name }, + }) + } + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+
+
+
+ ); }