From ff6a5f6741b7228bd246dd8faed57df6e06e017c Mon Sep 17 00:00:00 2001 From: phuongdm Date: Mon, 20 Oct 2025 16:46:17 +0700 Subject: [PATCH] refactor route --- index.html | 2 +- package-lock.json | 266 +++++++++++++ package.json | 2 + public/computer-956.svg | 6 + src/components/computer-card.tsx | 129 +++++++ src/components/device-grid.tsx | 54 +++ src/components/device-table.tsx | 225 +++++++++++ src/components/floor-select.tsx | 33 ++ src/components/room-select.tsx | 51 +++ src/components/ui/popover.tsx | 46 +++ src/components/ui/select.tsx | 185 +++++++++ src/config/api.ts | 8 + src/hooks/useFloor.ts | 24 ++ src/hooks/useMachineNumber.ts | 9 + src/layouts/app-layout.tsx | 19 +- src/routeTree.gen.ts | 22 ++ src/routes/_authenticated/blacklist/index.tsx | 69 +++- src/routes/_authenticated/device/index.tsx | 10 + .../_authenticated/room/$roomName/index.tsx | 350 ++---------------- 19 files changed, 1189 insertions(+), 321 deletions(-) create mode 100644 public/computer-956.svg create mode 100644 src/components/computer-card.tsx create mode 100644 src/components/device-grid.tsx create mode 100644 src/components/device-table.tsx create mode 100644 src/components/floor-select.tsx create mode 100644 src/components/room-select.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/hooks/useFloor.ts create mode 100644 src/hooks/useMachineNumber.ts create mode 100644 src/routes/_authenticated/device/index.tsx diff --git a/index.html b/index.html index 02b0d51..f7795a3 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + + + + + + \ No newline at end of file diff --git a/src/components/computer-card.tsx b/src/components/computer-card.tsx new file mode 100644 index 0000000..ac89b39 --- /dev/null +++ b/src/components/computer-card.tsx @@ -0,0 +1,129 @@ +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Badge } from "@/components/ui/badge"; +import { Monitor, Wifi, WifiOff } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export function ComputerCard({ + device, + position, +}: { + device: any | undefined; + position: number; +}) { + if (!device) { + return ( +
+
+ {position} +
+ + Trống +
+ ); + } + + const isOffline = device.isOffline; + const firstNetworkInfo = device.networkInfos?.[0]; + + const DeviceInfo = () => ( +
+
+
Thời gian thiết bị
+
+
{new Date(device.deviceTime).toLocaleDateString("vi-VN")}
+
+ {new Date(device.deviceTime).toLocaleTimeString("vi-VN")} +
+
+
+ +
+
Phiên bản
+ + v{device.version} + +
+ +
+
Phòng
+
{device.room}
+
+ + {device.networkInfos?.length > 0 && ( +
+
Thông tin mạng
+
+ {device.networkInfos.map((info: any, idx: number) => ( +
+
MAC: {info.macAddress ?? "-"}
+
IP: {info.ipAddress ?? "-"}
+
+ ))} +
+
+ )} + +
+
Trạng thái
+ + {isOffline ? : } + {isOffline ? "Offline" : "Online"} + +
+
+ ); + + return ( + + +
+
+ {position} +
+ + + {firstNetworkInfo?.ipAddress && ( +
+ {firstNetworkInfo.ipAddress} +
+ )} +
+ {isOffline ? ( + + ) : ( + + )} + + {isOffline ? "Off" : "On"} + +
+
+
+ + + +
+ ); +} diff --git a/src/components/device-grid.tsx b/src/components/device-grid.tsx new file mode 100644 index 0000000..254143c --- /dev/null +++ b/src/components/device-grid.tsx @@ -0,0 +1,54 @@ +import { Monitor, DoorOpen } from "lucide-react"; +import { ComputerCard } from "./computer-card"; +import { useMachineNumber } from "../hooks/useMachineNumber"; + +export function DeviceGrid({ devices }: { devices: any[] }) { + const getMachineNumber = useMachineNumber(); + const deviceMap = new Map(); + + devices.forEach((device) => { + const number = getMachineNumber(device.id || ""); + if (number > 0 && number <= 40) deviceMap.set(number, device); + }); + + const computersPerRow = 8; + const totalRows = 5; + + const renderRow = (rowIndex: number) => { + const start = rowIndex * computersPerRow + 1; + return ( +
+ {Array.from({ length: 4 }).map((_, i) => { + const pos = start + i; + return ; + })} +
+
+
+ {Array.from({ length: 4 }).map((_, i) => { + const pos = start + i + 4; + return ; + })} +
+ ); + }; + + return ( +
+
+
+ + Bàn Giảng Viên +
+
+ + Cửa Ra Vào +
+
+ +
+ {Array.from({ length: totalRows }).map((_, i) => renderRow(i))} +
+
+ ); +} diff --git a/src/components/device-table.tsx b/src/components/device-table.tsx new file mode 100644 index 0000000..4fbea37 --- /dev/null +++ b/src/components/device-table.tsx @@ -0,0 +1,225 @@ +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[] = [ + { + header: "STT", + cell: ({ row }) => { + const machineNumber = getMachineNumber(row.original.id || ""); + return ( +
+ + + + + + {machineNumber} +
+ ); + }, + }, + { + header: () => ( +
+ + Thời gian thiết bị +
+ ), + accessorKey: "deviceTime", + cell: ({ getValue }) => { + const date = new Date(getValue() as string); + return ( +
+
{date.toLocaleDateString("vi-VN")}
+
{date.toLocaleTimeString("vi-VN")}
+
+ ); + }, + }, + { + header: "Phiên bản", + accessorKey: "version", + cell: ({ getValue }) => ( + + v{getValue() as string} + + ), + }, + { + header: () => ( +
+ + Phòng +
+ ), + accessorKey: "room", + cell: ({ getValue }) => {getValue() as string}, + }, + { + header: () => ( +
+ + Thông tin mạng +
+ ), + accessorKey: "networkInfos", + cell: ({ getValue }) => { + const infos = getValue() as { macAddress?: string; ipAddress?: string }[]; + if (!infos || infos.length === 0) { + return Không có dữ liệu; + } + return ( +
+ {infos.map((info, idx) => ( +
+ + + {info.macAddress ?? "-"} + + + + {info.ipAddress ?? "-"} + +
+ ))} +
+ ); + }, + }, + { + header: "Trạng thái", + accessorKey: "isOffline", + cell: ({ getValue }) => { + const isOffline = getValue() as boolean; + return ( + + {isOffline ? : } + {isOffline ? "Offline" : "Online"} + + ); + }, + }, + ]; + + const table = useReactTable({ + data: devices, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + initialState: { pagination: { pageSize: 16 } }, + }); + + return ( +
+ + + {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())} + + ))} + + ))} + +
+ + {/* Pagination */} +
+
+ + 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ị + +
+ +
+ + +
+ Trang + + {table.getState().pagination.pageIndex + 1} + + của {table.getPageCount()} +
+ + +
+
+
+ ); +} diff --git a/src/components/floor-select.tsx b/src/components/floor-select.tsx new file mode 100644 index 0000000..fd31c5b --- /dev/null +++ b/src/components/floor-select.tsx @@ -0,0 +1,33 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface FloorSelectProps { + selectedFloor?: string; + onChange: (value: string) => void; + floors: number[]; +} + +/** + * Component chọn tầng (render động từ floors) + */ +export function FloorSelect({ selectedFloor, onChange, floors }: FloorSelectProps) { + return ( + + ); +} diff --git a/src/components/room-select.tsx b/src/components/room-select.tsx new file mode 100644 index 0000000..c9dad0d --- /dev/null +++ b/src/components/room-select.tsx @@ -0,0 +1,51 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +import type { Room } from "@/types/room"; + +interface RoomSelectProps { + selectedRoom?: string; + onChange: (value: string) => void; + rooms: Room[]; + selectedFloor?: string; +} + +/** + * Component chọn phòng — tự động lọc theo tầng đã chọn + */ +export function RoomSelect({ + selectedRoom, + onChange, + rooms, + selectedFloor, +}: RoomSelectProps) { + const filteredRooms = selectedFloor + ? rooms.filter((room) => room.name.startsWith(selectedFloor)) + : []; + + return ( + + ); +} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..6d51b6c --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..d34798f --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,185 @@ +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + align = "center", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/src/config/api.ts b/src/config/api.ts index 4fe218f..de4e355 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -9,9 +9,15 @@ export const API_ENDPOINTS = { GET_VERSION: `${BASE_URL}/AppVersion/version`, UPLOAD: `${BASE_URL}/AppVersion/upload`, GET_SOFTWARE: `${BASE_URL}/AppVersion/msifiles`, + GET_BLACKLIST: `${BASE_URL}/AppVersion/blacklist`, + ADD_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/add`, + DELETE_BLACKLIST: (appId: string) => `${BASE_URL}/AppVersion/blacklist/remove/${appId}`, + UPDATE_BLACKLIST: (appId: string) => `${BASE_URL}/AppVersion/blacklist/update/${appId}`, + REQUEST_UPDATE_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/request-update`, }, DEVICE_COMM: { DOWNLOAD_MSI: (roomName: string) => `${BASE_URL}/DeviceComm/installmsi/${roomName}`, + GET_ALL_DEVICES: `${BASE_URL}/DeviceComm/alldevices`, GET_ROOM_LIST: `${BASE_URL}/DeviceComm/rooms`, GET_DEVICE_FROM_ROOM: (roomName: string) => `${BASE_URL}/DeviceComm/room/${roomName}`, @@ -22,5 +28,7 @@ export const API_ENDPOINTS = { SSE_EVENTS: { DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`, DEVICE_OFFLINE: `${BASE_URL}/Sse/events/offlineDevices`, + GET_PROCESSES_LISTS: `${BASE_URL}/Sse/events/processLists`, + }, }; diff --git a/src/hooks/useFloor.ts b/src/hooks/useFloor.ts new file mode 100644 index 0000000..73689bb --- /dev/null +++ b/src/hooks/useFloor.ts @@ -0,0 +1,24 @@ +import { useMemo } from "react"; +import type { Room } from "@/types/room"; + +/** + * Trích xuất danh sách tầng từ danh sách phòng (mảng object) + */ +export function useFloors(rooms: Room[]) { + const floors = useMemo(() => { + if (!rooms || rooms.length === 0) return []; + + const extracted = rooms.map((room) => { + const name = room.name || ""; + if (name.length === 4) return parseInt(name.slice(0, 2), 10); + if (name.length === 3) return parseInt(name.slice(0, 1), 10); + return 0; + }); + + return Array.from(new Set(extracted)) + .filter((f) => f > 0) + .sort((a, b) => a - b); + }, [rooms]); + + return floors; +} diff --git a/src/hooks/useMachineNumber.ts b/src/hooks/useMachineNumber.ts new file mode 100644 index 0000000..0576048 --- /dev/null +++ b/src/hooks/useMachineNumber.ts @@ -0,0 +1,9 @@ +/** + * Lấy số máy từ deviceId (VD: "PC_M10" → 10) + */ +export function useMachineNumber() { + return (deviceId: string): number => { + const match = deviceId.match(/M(\d+)/); + return match ? Number.parseInt(match[1], 10) : 0; + }; +} diff --git a/src/layouts/app-layout.tsx b/src/layouts/app-layout.tsx index c81b711..5ad3e16 100644 --- a/src/layouts/app-layout.tsx +++ b/src/layouts/app-layout.tsx @@ -5,7 +5,7 @@ import { SidebarInset, SidebarTrigger, } from "@/components/ui/sidebar"; -import { Home, Building, AppWindow, Terminal } from "lucide-react"; +import { Home, Building, AppWindow, Terminal, CircleX } from "lucide-react"; import { Toaster } from "@/components/ui/sonner"; import { useQueryClient } from "@tanstack/react-query"; import { API_ENDPOINTS, BASE_URL } from "@/config/api"; @@ -50,6 +50,17 @@ export default function AppLayout({ children }: AppLayoutProps) { }); }; + const handlePrefetchBannedSoftware = () => { + queryClient.prefetchQuery({ + queryKey: ["blacklist"], + queryFn: () => + fetch(BASE_URL + API_ENDPOINTS.APP_VERSION).then((res) => + res.json() + ), + staleTime: 60 * 1000, + }); + }; + const items = [ { title: "Dashboard", to: "/", icon: Home }, { @@ -71,6 +82,12 @@ export default function AppLayout({ children }: AppLayoutProps) { onPointerEnter: handlePrefetchSofware, }, { title: "Gửi lệnh CMD", to: "/command", icon: Terminal }, + { + title: "Danh sách đen", + to: "/blacklist", + icon: CircleX, + onPointerEnter: handlePrefetchBannedSoftware, + }, ]; return ( diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index bed6aa4..bc97134 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -13,6 +13,7 @@ import { Route as AuthenticatedRouteImport } from './routes/_authenticated' import { Route as AuthRouteImport } from './routes/_auth' import { Route as IndexRouteImport } from './routes/index' import { Route as AuthenticatedRoomIndexRouteImport } from './routes/_authenticated/room/index' +import { Route as AuthenticatedDeviceIndexRouteImport } from './routes/_authenticated/device/index' import { Route as AuthenticatedCommandIndexRouteImport } from './routes/_authenticated/command/index' import { Route as AuthenticatedBlacklistIndexRouteImport } from './routes/_authenticated/blacklist/index' import { Route as AuthenticatedAppsIndexRouteImport } from './routes/_authenticated/apps/index' @@ -38,6 +39,12 @@ const AuthenticatedRoomIndexRoute = AuthenticatedRoomIndexRouteImport.update({ path: '/room/', getParentRoute: () => AuthenticatedRoute, } as any) +const AuthenticatedDeviceIndexRoute = + AuthenticatedDeviceIndexRouteImport.update({ + id: '/device/', + path: '/device/', + getParentRoute: () => AuthenticatedRoute, + } as any) const AuthenticatedCommandIndexRoute = AuthenticatedCommandIndexRouteImport.update({ id: '/command/', @@ -79,6 +86,7 @@ export interface FileRoutesByFullPath { '/apps': typeof AuthenticatedAppsIndexRoute '/blacklist': typeof AuthenticatedBlacklistIndexRoute '/command': typeof AuthenticatedCommandIndexRoute + '/device': typeof AuthenticatedDeviceIndexRoute '/room': typeof AuthenticatedRoomIndexRoute '/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute } @@ -89,6 +97,7 @@ export interface FileRoutesByTo { '/apps': typeof AuthenticatedAppsIndexRoute '/blacklist': typeof AuthenticatedBlacklistIndexRoute '/command': typeof AuthenticatedCommandIndexRoute + '/device': typeof AuthenticatedDeviceIndexRoute '/room': typeof AuthenticatedRoomIndexRoute '/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute } @@ -102,6 +111,7 @@ export interface FileRoutesById { '/_authenticated/apps/': typeof AuthenticatedAppsIndexRoute '/_authenticated/blacklist/': typeof AuthenticatedBlacklistIndexRoute '/_authenticated/command/': typeof AuthenticatedCommandIndexRoute + '/_authenticated/device/': typeof AuthenticatedDeviceIndexRoute '/_authenticated/room/': typeof AuthenticatedRoomIndexRoute '/_authenticated/room/$roomName/': typeof AuthenticatedRoomRoomNameIndexRoute } @@ -114,6 +124,7 @@ export interface FileRouteTypes { | '/apps' | '/blacklist' | '/command' + | '/device' | '/room' | '/room/$roomName' fileRoutesByTo: FileRoutesByTo @@ -124,6 +135,7 @@ export interface FileRouteTypes { | '/apps' | '/blacklist' | '/command' + | '/device' | '/room' | '/room/$roomName' id: @@ -136,6 +148,7 @@ export interface FileRouteTypes { | '/_authenticated/apps/' | '/_authenticated/blacklist/' | '/_authenticated/command/' + | '/_authenticated/device/' | '/_authenticated/room/' | '/_authenticated/room/$roomName/' fileRoutesById: FileRoutesById @@ -176,6 +189,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedRoomIndexRouteImport parentRoute: typeof AuthenticatedRoute } + '/_authenticated/device/': { + id: '/_authenticated/device/' + path: '/device' + fullPath: '/device' + preLoaderRoute: typeof AuthenticatedDeviceIndexRouteImport + parentRoute: typeof AuthenticatedRoute + } '/_authenticated/command/': { id: '/_authenticated/command/' path: '/command' @@ -236,6 +256,7 @@ interface AuthenticatedRouteChildren { AuthenticatedAppsIndexRoute: typeof AuthenticatedAppsIndexRoute AuthenticatedBlacklistIndexRoute: typeof AuthenticatedBlacklistIndexRoute AuthenticatedCommandIndexRoute: typeof AuthenticatedCommandIndexRoute + AuthenticatedDeviceIndexRoute: typeof AuthenticatedDeviceIndexRoute AuthenticatedRoomIndexRoute: typeof AuthenticatedRoomIndexRoute AuthenticatedRoomRoomNameIndexRoute: typeof AuthenticatedRoomRoomNameIndexRoute } @@ -245,6 +266,7 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { AuthenticatedAppsIndexRoute: AuthenticatedAppsIndexRoute, AuthenticatedBlacklistIndexRoute: AuthenticatedBlacklistIndexRoute, AuthenticatedCommandIndexRoute: AuthenticatedCommandIndexRoute, + AuthenticatedDeviceIndexRoute: AuthenticatedDeviceIndexRoute, AuthenticatedRoomIndexRoute: AuthenticatedRoomIndexRoute, AuthenticatedRoomRoomNameIndexRoute: AuthenticatedRoomRoomNameIndexRoute, } diff --git a/src/routes/_authenticated/blacklist/index.tsx b/src/routes/_authenticated/blacklist/index.tsx index 5a7c25d..2b0d634 100644 --- a/src/routes/_authenticated/blacklist/index.tsx +++ b/src/routes/_authenticated/blacklist/index.tsx @@ -1,9 +1,66 @@ -import { createFileRoute } from '@tanstack/react-router' +import { API_ENDPOINTS, BASE_URL } from "@/config/api"; +import { useQueryData } from "@/hooks/useQueryData"; +import { createFileRoute } from "@tanstack/react-router"; +import type { ColumnDef } from "@tanstack/react-table"; +import { useState } from "react"; -export const Route = createFileRoute('/_authenticated/blacklist/')({ - component: RouteComponent, -}) +type Blacklist = { + id: number; + appName: string; + processName: string; + createdAt?: string; + updatedAt?: string; + createdBy?: string; +}; -function RouteComponent() { - return
Hello "/_authenticated/blacklist/"!
+export const Route = createFileRoute("/_authenticated/blacklist/")({ + head: () => ({ meta: [{ title: "Danh sách các ứng dụng bị chặn" }] }), + component: BlacklistComponent, +}); + +function BlacklistComponent() { + const { data, isLoading } = useQueryData({ + queryKey: ["blacklist"], + url: BASE_URL + API_ENDPOINTS.APP_VERSION.GET_VERSION, + }); + + const blacklist: Blacklist[] = Array.isArray(data) + ? (data as Blacklist[]) + : []; + + const columns : ColumnDef[] = + [ + { + accessorKey: "id", + header: "ID", + cell: info => info.getValue(), + }, + { + accessorKey: "appName", + header: "Tên ứng dụng", + cell: info => info.getValue(), + }, + { + accessorKey: "processName", + header: "Tên tiến trình", + cell: info => info.getValue(), + }, + { + accessorKey: "createdAt", + header: "Ngày tạo", + cell: info => info.getValue(), + }, + { + accessorKey: "updatedAt", + header: "Ngày cập nhật", + cell: info => info.getValue(), + }, + { + accessorKey: "createdBy", + header: "Người tạo", + cell: info => info.getValue(), + }, + ] + + return
Hello "/_authenticated/blacklist/"!
; } diff --git a/src/routes/_authenticated/device/index.tsx b/src/routes/_authenticated/device/index.tsx new file mode 100644 index 0000000..2ffcd63 --- /dev/null +++ b/src/routes/_authenticated/device/index.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authenticated/device/')({ + head: () => ({ meta: [{ title: 'Danh sách tất cả thiết bị' }] }), + component: AllDevicesComponent, +}) + +function AllDevicesComponent() { + return
Hello "/_authenticated/device/"!
+} diff --git a/src/routes/_authenticated/room/$roomName/index.tsx b/src/routes/_authenticated/room/$roomName/index.tsx index e621ad2..00b2369 100644 --- a/src/routes/_authenticated/room/$roomName/index.tsx +++ b/src/routes/_authenticated/room/$roomName/index.tsx @@ -1,245 +1,59 @@ import { createFileRoute, useParams } from "@tanstack/react-router"; +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { LayoutGrid, TableIcon, Monitor } from "lucide-react"; +import { Button } from "@/components/ui/button"; import { useQueryData } from "@/hooks/useQueryData"; import { API_ENDPOINTS, BASE_URL } from "@/config/api"; -import { - flexRender, - getCoreRowModel, - getPaginationRowModel, - 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"; -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"; +import { DeviceGrid } from "@/components/device-grid"; +import { DeviceTable } from "@/components/device-table"; export const Route = createFileRoute("/_authenticated/room/$roomName/")({ head: ({ params }) => ({ meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }], }), - component: RoomDetailComponent, + component: RoomDetailPage, }); -function RoomDetailComponent() { +function RoomDetailPage() { const { roomName } = useParams({ from: "/_authenticated/room/$roomName/" }); - - const { data: devices = [], isLoading } = useQueryData({ + const [viewMode, setViewMode] = useState<"table" | "grid">("table"); + const { data: devices = [] } = useQueryData({ queryKey: ["devices", roomName], url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.GET_DEVICE_FROM_ROOM(roomName), }); - // Lắng nghe SSE và update state - useDeviceEvents(roomName); - - const columns: ColumnDef[] = [ - { - header: "STT", - cell: ({ row }) => ( -
- - - - - - {row.index + 1} -
- ), - }, - { - header: () => ( -
- - Thời gian thiết bị -
- ), - accessorKey: "deviceTime", - cell: ({ getValue }) => { - const date = new Date(getValue() as string); - return ( -
-
- {date.toLocaleDateString("vi-VN")} -
-
- {date.toLocaleTimeString("vi-VN")} -
-
- ); - }, - }, - { - header: "Phiên bản", - accessorKey: "version", - cell: ({ getValue }) => ( - - v{getValue() as string} - - ), - }, - { - header: () => ( -
- - Phòng -
- ), - accessorKey: "room", - cell: ({ getValue }) => ( - {getValue() as string} - ), - }, - { - header: () => ( -
- - Thông tin mạng -
- ), - accessorKey: "networkInfos", - cell: ({ getValue }) => { - const networkInfos = getValue() as { - macAddress?: string; - ipAddress?: string; - }[]; - - if (!networkInfos || networkInfos.length === 0) { - return ( - - Không có dữ liệu - - ); - } - - return ( -
- {networkInfos.map((info, idx) => ( -
- - - {info.macAddress ?? "-"} - - - - {info.ipAddress ?? "-"} - -
- ))} -
- ); - }, - }, - { - header: "Trạng thái", - accessorKey: "isOffline", - cell: ({ getValue }) => { - const isOffline = getValue() as boolean; - return ( - - {isOffline ? ( - - ) : ( - - )} - {isOffline ? "Offline" : "Online"} - - ); - }, - }, - ]; - - const table = useReactTable({ - data: devices, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - initialState: { pagination: { pageSize: 16 } }, - }); - - if (isLoading) { - return ( -
-
-
- -

Đang tải danh sách phòng...

-
-
-
- ); - } - - const onlineDevices = devices.filter( - (device: any) => !device.isOffline - ).length; - const offlineDevices = devices.length - onlineDevices; - return (
-
-
-

- Phòng: {roomName} -

-

- Quản lý và theo dõi thiết bị trong phòng -

-
-
-
-
- {onlineDevices} -
-
Online
-
-
-
- {offlineDevices} -
-
Offline
-
-
-
{devices.length}
-
Tổng cộng
-
-
-
- - + - Danh sách thiết bị + Danh sách thiết bị phòng {roomName} + +
+ + +
+ {devices.length === 0 ? (
@@ -249,100 +63,10 @@ function RoomDetailComponent() { Phòng này chưa có thiết bị nào được kết nối.

+ ) : viewMode === "grid" ? ( + ) : ( - <> -
- - - {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() - )} - - ))} - - ))} - -
-
- -
-
- - 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ị - -
- -
- - -
- Trang - - {table.getState().pagination.pageIndex + 1} - - của {table.getPageCount()} -
- - -
-
- + )}