diff --git a/src/components/buttons/command-action-buttons.tsx b/src/components/buttons/command-action-buttons.tsx index 569c4b7..0a76646 100644 --- a/src/components/buttons/command-action-buttons.tsx +++ b/src/components/buttons/command-action-buttons.tsx @@ -109,7 +109,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command // All rendered commands are sourced from sensitiveCommands — send via sensitive mutation await executeSensitiveMutation.mutateAsync({ roomName, - command: confirmDialog.command.commandContent, + command: confirmDialog.command.commandName, password: sensitivePassword, }); diff --git a/src/components/columns/agent-column.tsx b/src/components/columns/agent-column.tsx new file mode 100644 index 0000000..ca39cee --- /dev/null +++ b/src/components/columns/agent-column.tsx @@ -0,0 +1,23 @@ +import type { Version } from "@/types/file"; +import type { ColumnDef } from "@tanstack/react-table"; + +export const agentColumns: ColumnDef[] = [ + { accessorKey: "version", header: "Phiên bản" }, + { accessorKey: "fileName", header: "Tên file" }, + { + accessorKey: "updatedAt", + header: "Thời gian cập nhật", + cell: ({ getValue }) => + getValue() + ? new Date(getValue() as string).toLocaleString("vi-VN") + : "N/A", + }, + { + accessorKey: "requestUpdateAt", + header: "Thời gian yêu cầu cập nhật", + cell: ({ getValue }) => + getValue() + ? new Date(getValue() as string).toLocaleString("vi-VN") + : "N/A", + }, + ]; \ No newline at end of file diff --git a/src/components/columns/apps-column.tsx b/src/components/columns/apps-column.tsx new file mode 100644 index 0000000..667c993 --- /dev/null +++ b/src/components/columns/apps-column.tsx @@ -0,0 +1,70 @@ +// components/columns/apps-column.tsx +import type { Version } from "@/types/file"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Check, X } from "lucide-react"; + +// Không gọi hook ở đây — nhận isPending từ ngoài truyền vào +export function createAppsColumns(isPending: boolean): ColumnDef[] { + return [ + { accessorKey: "version", header: "Phiên bản" }, + { accessorKey: "fileName", header: "Tên file" }, + { + accessorKey: "updatedAt", + header: () => ( +
Thời gian cập nhật
+ ), + cell: ({ getValue }) => + getValue() + ? new Date(getValue() as string).toLocaleString("vi-VN") + : "N/A", + }, + { + accessorKey: "requestUpdateAt", + header: () => ( +
+ Thời gian yêu cầu cài đặt/tải xuống +
+ ), + cell: ({ getValue }) => + getValue() + ? new Date(getValue() as string).toLocaleString("vi-VN") + : "N/A", + }, + { + id: "required", + header: () => ( +
Đã thêm vào danh sách
+ ), + cell: ({ row }) => { + const isRequired = row.original.isRequired; + return isRequired ? ( +
+ + +
+ ) : ( +
+ + Không +
+ ); + }, + enableSorting: false, + enableHiding: false, + }, + { + id: "select", + header: () =>
Chọn
, + cell: ({ row }) => ( + + ), + enableSorting: false, + enableHiding: false, + }, + ]; +} \ No newline at end of file diff --git a/src/components/columns/audit-column.tsx b/src/components/columns/audit-column.tsx new file mode 100644 index 0000000..6407463 --- /dev/null +++ b/src/components/columns/audit-column.tsx @@ -0,0 +1,98 @@ +import { type ColumnDef } from "@tanstack/react-table"; +import { Badge } from "@/components/ui/badge"; +import type { Audits } from "@/types/audit"; + +export const auditColumns: ColumnDef[] = [ + { + header: "Thời gian", + accessorKey: "dateTime", + cell: ({ getValue }) => { + const v = getValue() as string; + const d = v ? new Date(v) : null; + return d ? ( +
+
{d.toLocaleDateString("vi-VN")}
+
+ {d.toLocaleTimeString("vi-VN")} +
+
+ ) : ( + + ); + }, + }, + { + header: "User", + accessorKey: "username", + cell: ({ getValue }) => ( + + {getValue() as string} + + ), + }, + { + header: "Loại", + accessorKey: "apiCall", + cell: ({ getValue }) => { + const v = (getValue() as string) ?? ""; + if (!v) return ; + return ( + + {v} + + ); + }, + }, + { + header: "Hành động", + accessorKey: "action", + cell: ({ getValue }) => ( + + {getValue() as string} + + ), + }, + { + header: "URL", + accessorKey: "url", + cell: ({ getValue }) => ( + + {(getValue() as string) ?? "—"} + + ), + }, + { + header: "Kết quả", + accessorKey: "isSuccess", + cell: ({ getValue }) => { + const v = getValue(); + if (v == null) return ; + return v ? ( + + Thành công + + ) : ( + + Thất bại + + ); + }, + }, + { + header: "Nội dung request", + accessorKey: "requestPayload", + cell: ({ getValue }) => { + const v = getValue() as string; + if (!v) return ; + let preview = v; + try { + preview = JSON.stringify(JSON.parse(v)); + } catch {} + return ( + + {preview} + + ); + }, + }, +]; \ No newline at end of file diff --git a/src/components/dialogs/audit-detail-dialog.tsx b/src/components/dialogs/audit-detail-dialog.tsx new file mode 100644 index 0000000..077db08 --- /dev/null +++ b/src/components/dialogs/audit-detail-dialog.tsx @@ -0,0 +1,180 @@ +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Separator } from "@/components/ui/separator"; +import type { Audits } from "@/types/audit"; + +function JsonDisplay({ value }: { value: string | null | undefined }) { + if (!value) return ; + try { + return ( +
+        {JSON.stringify(JSON.parse(value), null, 2)}
+      
+ ); + } catch { + return {value}; + } +} + +interface AuditDetailDialogProps { + audit: Audits | null; + open: boolean; + onClose: () => void; +} + +export function AuditDetailDialog({ + audit, + open, + onClose, +}: AuditDetailDialogProps) { + if (!audit) return null; + + + + return ( + !o && onClose()}> + + + + Chi tiết audit + + #{audit.id} + + + + + + +
+
+

+ Thời gian +

+

+ {audit.dateTime + ? new Date(audit.dateTime).toLocaleString("vi-VN") + : "—"} +

+
+ +
+

+ User +

+

{audit.username}

+
+ +
+

+ API Call +

+ + {audit.apiCall ?? "—"} + +
+ +
+

+ Kết quả +

+
+ {audit.isSuccess == null ? ( + + ) : audit.isSuccess ? ( + + Thành công + + ) : ( + + Thất bại + + )} +
+
+ +
+

+ Hành động +

+ + {audit.action} + +
+ +
+

+ URL +

+ + {audit.url ?? "—"} + +
+ +
+

+ Bảng +

+

{audit.tableName ?? "—"}

+
+ +
+

+ Entity ID +

+

{audit.entityId ?? "—"}

+
+ +
+

+ Lỗi +

+

{audit.errorMessage ?? "—"}

+
+
+ + + +
+
+

+ Nội dung request +

+ +
+ +
+

+ Giá trị cũ +

+ +
+ +
+

+ Giá trị mới +

+ +
+ +
+

+ Kết quả +

+

{audit.isSuccess == null ? "—" : audit.isSuccess ? "Thành công" : "Thất bại"}

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/filters/audit-filter-bar.tsx b/src/components/filters/audit-filter-bar.tsx new file mode 100644 index 0000000..69b0a4b --- /dev/null +++ b/src/components/filters/audit-filter-bar.tsx @@ -0,0 +1,73 @@ +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +interface AuditFilterBarProps { + username: string | null; + action: string | null; + from: string | null; + to: string | null; + isLoading: boolean; + isFetching: boolean; + onUsernameChange: (v: string | null) => void; + onActionChange: (v: string | null) => void; + onFromChange: (v: string | null) => void; + onToChange: (v: string | null) => void; + onSearch: () => void; + onReset: () => void; +} + +export function AuditFilterBar({ + username, + action, + from, + to, + isLoading, + isFetching, + onUsernameChange, + onActionChange, + onFromChange, + onToChange, + onSearch, + onReset, +}: AuditFilterBarProps) { + return ( +
+ onUsernameChange(e.target.value || null)} + /> + + onActionChange(e.target.value || null)} + /> + + onFromChange(e.target.value || null)} + /> + + onToChange(e.target.value || null)} + /> + +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/src/config/api.ts b/src/config/api.ts index 6af1583..44be9f2 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -82,4 +82,12 @@ export const API_ENDPOINTS = { TOGGLE_PERMISSION: (roleId: number, permissionId: number) => `${BASE_URL}/Role/${roleId}/permissions/${permissionId}/toggle`, }, + DASHBOARD: + { + + } + , + AUDIT: { + GET_AUDITS: `${BASE_URL}/Audit/audits`, + } }; diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index 3804d03..0b2abc2 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -10,6 +10,9 @@ export * from "./useDeviceCommQueries"; // Command Queries export * from "./useCommandQueries"; +// Audit Queries +export * from "./useAuditQueries"; + // Permission Queries export * from "./usePermissionQueries"; diff --git a/src/hooks/queries/useAuditQueries.ts b/src/hooks/queries/useAuditQueries.ts new file mode 100644 index 0000000..68d16f5 --- /dev/null +++ b/src/hooks/queries/useAuditQueries.ts @@ -0,0 +1,37 @@ +import { useQuery } from "@tanstack/react-query"; +import * as auditService from "@/services/audit.service"; +import type { PageResult, Audits } from "@/types/audit"; + +const AUDIT_QUERY_KEYS = { + all: ["audit"] as const, + list: () => [...AUDIT_QUERY_KEYS.all, "list"] as const, + audits: (params: any) => [...AUDIT_QUERY_KEYS.all, "audits", params] as const, +}; + +export function useGetAudits( + params: { + pageNumber?: number; + pageSize?: number; + username?: string | null; + action?: string | null; + from?: string | null; + to?: string | null; + } = { pageNumber: 1, pageSize: 20 }, + enabled = true +) { + const { pageNumber = 1, pageSize = 20, username, action, from, to } = params; + + return useQuery>({ + queryKey: AUDIT_QUERY_KEYS.audits({ pageNumber, pageSize, username, action, from, to }), + queryFn: () => + auditService.getAudits( + pageNumber, + pageSize, + username ?? null, + action ?? null, + from ?? null, + to ?? null + ), + enabled, + }); +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 1cdba4b..8f40aa5 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -18,6 +18,7 @@ import { Route as AuthDeviceIndexRouteImport } from './routes/_auth/device/index import { Route as AuthDashboardIndexRouteImport } from './routes/_auth/dashboard/index' import { Route as AuthCommandsIndexRouteImport } from './routes/_auth/commands/index' import { Route as AuthBlacklistsIndexRouteImport } from './routes/_auth/blacklists/index' +import { Route as AuthAuditsIndexRouteImport } from './routes/_auth/audits/index' import { Route as AuthAppsIndexRouteImport } from './routes/_auth/apps/index' import { Route as AuthAgentIndexRouteImport } from './routes/_auth/agent/index' import { Route as authLoginIndexRouteImport } from './routes/(auth)/login/index' @@ -29,6 +30,7 @@ import { Route as AuthProfileUserNameIndexRouteImport } from './routes/_auth/pro import { Route as AuthUserRoleRoleIdIndexRouteImport } from './routes/_auth/user/role/$roleId/index' import { Route as AuthUserChangePasswordUserNameIndexRouteImport } from './routes/_auth/user/change-password/$userName/index' import { Route as AuthRoomsRoomNameFolderStatusIndexRouteImport } from './routes/_auth/rooms/$roomName/folder-status/index' +import { Route as AuthRoomsRoomNameConnectIndexRouteImport } from './routes/_auth/rooms/$roomName/connect/index' import { Route as AuthRoleIdEditIndexRouteImport } from './routes/_auth/role/$id/edit/index' const AuthRoute = AuthRouteImport.update({ @@ -75,6 +77,11 @@ const AuthBlacklistsIndexRoute = AuthBlacklistsIndexRouteImport.update({ path: '/blacklists/', getParentRoute: () => AuthRoute, } as any) +const AuthAuditsIndexRoute = AuthAuditsIndexRouteImport.update({ + id: '/audits/', + path: '/audits/', + getParentRoute: () => AuthRoute, +} as any) const AuthAppsIndexRoute = AuthAppsIndexRouteImport.update({ id: '/apps/', path: '/apps/', @@ -134,6 +141,12 @@ const AuthRoomsRoomNameFolderStatusIndexRoute = path: '/rooms/$roomName/folder-status/', getParentRoute: () => AuthRoute, } as any) +const AuthRoomsRoomNameConnectIndexRoute = + AuthRoomsRoomNameConnectIndexRouteImport.update({ + id: '/rooms/$roomName/connect/', + path: '/rooms/$roomName/connect/', + getParentRoute: () => AuthRoute, + } as any) const AuthRoleIdEditIndexRoute = AuthRoleIdEditIndexRouteImport.update({ id: '/role/$id/edit/', path: '/role/$id/edit/', @@ -145,6 +158,7 @@ export interface FileRoutesByFullPath { '/login': typeof authLoginIndexRoute '/agent': typeof AuthAgentIndexRoute '/apps': typeof AuthAppsIndexRoute + '/audits': typeof AuthAuditsIndexRoute '/blacklists': typeof AuthBlacklistsIndexRoute '/commands': typeof AuthCommandsIndexRoute '/dashboard': typeof AuthDashboardIndexRoute @@ -158,6 +172,7 @@ export interface FileRoutesByFullPath { '/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute '/user/create': typeof AuthUserCreateIndexRoute '/role/$id/edit': typeof AuthRoleIdEditIndexRoute + '/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute '/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute '/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute '/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute @@ -167,6 +182,7 @@ export interface FileRoutesByTo { '/login': typeof authLoginIndexRoute '/agent': typeof AuthAgentIndexRoute '/apps': typeof AuthAppsIndexRoute + '/audits': typeof AuthAuditsIndexRoute '/blacklists': typeof AuthBlacklistsIndexRoute '/commands': typeof AuthCommandsIndexRoute '/dashboard': typeof AuthDashboardIndexRoute @@ -180,6 +196,7 @@ export interface FileRoutesByTo { '/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute '/user/create': typeof AuthUserCreateIndexRoute '/role/$id/edit': typeof AuthRoleIdEditIndexRoute + '/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute '/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute '/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute '/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute @@ -191,6 +208,7 @@ export interface FileRoutesById { '/(auth)/login/': typeof authLoginIndexRoute '/_auth/agent/': typeof AuthAgentIndexRoute '/_auth/apps/': typeof AuthAppsIndexRoute + '/_auth/audits/': typeof AuthAuditsIndexRoute '/_auth/blacklists/': typeof AuthBlacklistsIndexRoute '/_auth/commands/': typeof AuthCommandsIndexRoute '/_auth/dashboard/': typeof AuthDashboardIndexRoute @@ -204,6 +222,7 @@ export interface FileRoutesById { '/_auth/rooms/$roomName/': typeof AuthRoomsRoomNameIndexRoute '/_auth/user/create/': typeof AuthUserCreateIndexRoute '/_auth/role/$id/edit/': typeof AuthRoleIdEditIndexRoute + '/_auth/rooms/$roomName/connect/': typeof AuthRoomsRoomNameConnectIndexRoute '/_auth/rooms/$roomName/folder-status/': typeof AuthRoomsRoomNameFolderStatusIndexRoute '/_auth/user/change-password/$userName/': typeof AuthUserChangePasswordUserNameIndexRoute '/_auth/user/role/$roleId/': typeof AuthUserRoleRoleIdIndexRoute @@ -215,6 +234,7 @@ export interface FileRouteTypes { | '/login' | '/agent' | '/apps' + | '/audits' | '/blacklists' | '/commands' | '/dashboard' @@ -228,6 +248,7 @@ export interface FileRouteTypes { | '/rooms/$roomName' | '/user/create' | '/role/$id/edit' + | '/rooms/$roomName/connect' | '/rooms/$roomName/folder-status' | '/user/change-password/$userName' | '/user/role/$roleId' @@ -237,6 +258,7 @@ export interface FileRouteTypes { | '/login' | '/agent' | '/apps' + | '/audits' | '/blacklists' | '/commands' | '/dashboard' @@ -250,6 +272,7 @@ export interface FileRouteTypes { | '/rooms/$roomName' | '/user/create' | '/role/$id/edit' + | '/rooms/$roomName/connect' | '/rooms/$roomName/folder-status' | '/user/change-password/$userName' | '/user/role/$roleId' @@ -260,6 +283,7 @@ export interface FileRouteTypes { | '/(auth)/login/' | '/_auth/agent/' | '/_auth/apps/' + | '/_auth/audits/' | '/_auth/blacklists/' | '/_auth/commands/' | '/_auth/dashboard/' @@ -273,6 +297,7 @@ export interface FileRouteTypes { | '/_auth/rooms/$roomName/' | '/_auth/user/create/' | '/_auth/role/$id/edit/' + | '/_auth/rooms/$roomName/connect/' | '/_auth/rooms/$roomName/folder-status/' | '/_auth/user/change-password/$userName/' | '/_auth/user/role/$roleId/' @@ -349,6 +374,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthBlacklistsIndexRouteImport parentRoute: typeof AuthRoute } + '/_auth/audits/': { + id: '/_auth/audits/' + path: '/audits' + fullPath: '/audits' + preLoaderRoute: typeof AuthAuditsIndexRouteImport + parentRoute: typeof AuthRoute + } '/_auth/apps/': { id: '/_auth/apps/' path: '/apps' @@ -426,6 +458,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthRoomsRoomNameFolderStatusIndexRouteImport parentRoute: typeof AuthRoute } + '/_auth/rooms/$roomName/connect/': { + id: '/_auth/rooms/$roomName/connect/' + path: '/rooms/$roomName/connect' + fullPath: '/rooms/$roomName/connect' + preLoaderRoute: typeof AuthRoomsRoomNameConnectIndexRouteImport + parentRoute: typeof AuthRoute + } '/_auth/role/$id/edit/': { id: '/_auth/role/$id/edit/' path: '/role/$id/edit' @@ -439,6 +478,7 @@ declare module '@tanstack/react-router' { interface AuthRouteChildren { AuthAgentIndexRoute: typeof AuthAgentIndexRoute AuthAppsIndexRoute: typeof AuthAppsIndexRoute + AuthAuditsIndexRoute: typeof AuthAuditsIndexRoute AuthBlacklistsIndexRoute: typeof AuthBlacklistsIndexRoute AuthCommandsIndexRoute: typeof AuthCommandsIndexRoute AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute @@ -452,6 +492,7 @@ interface AuthRouteChildren { AuthRoomsRoomNameIndexRoute: typeof AuthRoomsRoomNameIndexRoute AuthUserCreateIndexRoute: typeof AuthUserCreateIndexRoute AuthRoleIdEditIndexRoute: typeof AuthRoleIdEditIndexRoute + AuthRoomsRoomNameConnectIndexRoute: typeof AuthRoomsRoomNameConnectIndexRoute AuthRoomsRoomNameFolderStatusIndexRoute: typeof AuthRoomsRoomNameFolderStatusIndexRoute AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute @@ -460,6 +501,7 @@ interface AuthRouteChildren { const AuthRouteChildren: AuthRouteChildren = { AuthAgentIndexRoute: AuthAgentIndexRoute, AuthAppsIndexRoute: AuthAppsIndexRoute, + AuthAuditsIndexRoute: AuthAuditsIndexRoute, AuthBlacklistsIndexRoute: AuthBlacklistsIndexRoute, AuthCommandsIndexRoute: AuthCommandsIndexRoute, AuthDashboardIndexRoute: AuthDashboardIndexRoute, @@ -473,6 +515,7 @@ const AuthRouteChildren: AuthRouteChildren = { AuthRoomsRoomNameIndexRoute: AuthRoomsRoomNameIndexRoute, AuthUserCreateIndexRoute: AuthUserCreateIndexRoute, AuthRoleIdEditIndexRoute: AuthRoleIdEditIndexRoute, + AuthRoomsRoomNameConnectIndexRoute: AuthRoomsRoomNameConnectIndexRoute, AuthRoomsRoomNameFolderStatusIndexRoute: AuthRoomsRoomNameFolderStatusIndexRoute, AuthUserChangePasswordUserNameIndexRoute: diff --git a/src/routes/_auth/agent/index.tsx b/src/routes/_auth/agent/index.tsx index ba9d54e..d7a2d9a 100644 --- a/src/routes/_auth/agent/index.tsx +++ b/src/routes/_auth/agent/index.tsx @@ -7,11 +7,10 @@ import { useUpdateAgent, } from "@/hooks/queries"; import { toast } from "sonner"; -import type { ColumnDef } from "@tanstack/react-table"; import type { AxiosProgressEvent } from "axios"; import type { Version } from "@/types/file"; import { ErrorFetchingPage } from "@/components/pages/error-fetching-page"; - +import { agentColumns } from "@/components/columns/agent-column"; export const Route = createFileRoute("/_auth/agent/")({ head: () => ({ meta: [{ title: "Quản lý Agent" }] }), component: AgentsPage, @@ -71,26 +70,7 @@ function AgentsPage() { }; // Cột bảng - const columns: ColumnDef[] = [ - { accessorKey: "version", header: "Phiên bản" }, - { accessorKey: "fileName", header: "Tên file" }, - { - accessorKey: "updatedAt", - header: "Thời gian cập nhật", - cell: ({ getValue }) => - getValue() - ? new Date(getValue() as string).toLocaleString("vi-VN") - : "N/A", - }, - { - accessorKey: "requestUpdateAt", - header: "Thời gian yêu cầu cập nhật", - cell: ({ getValue }) => - getValue() - ? new Date(getValue() as string).toLocaleString("vi-VN") - : "N/A", - }, - ]; + return ( @@ -98,7 +78,7 @@ function AgentsPage() { description="Quản lý và theo dõi các phiên bản Agent" data={versionList} isLoading={isLoading} - columns={columns} + columns={agentColumns} onUpload={handleUpload} onUpdate={handleUpdate} updateLoading={updateMutation.isPending} diff --git a/src/routes/_auth/apps/index.tsx b/src/routes/_auth/apps/index.tsx index 8ae6fe6..6f199e2 100644 --- a/src/routes/_auth/apps/index.tsx +++ b/src/routes/_auth/apps/index.tsx @@ -11,12 +11,10 @@ import { useDownloadFiles, } from "@/hooks/queries"; import { toast } from "sonner"; -import type { ColumnDef } from "@tanstack/react-table"; import type { AxiosProgressEvent } from "axios"; import type { Version } from "@/types/file"; -import { Check, X } from "lucide-react"; -import { useState } from "react"; - +import { useMemo, useState } from "react"; +import { createAppsColumns } from "@/components/columns/apps-column"; export const Route = createFileRoute("/_auth/apps/")({ head: () => ({ meta: [{ title: "Quản lý phần mềm" }] }), component: AppsComponent, @@ -51,62 +49,10 @@ function AppsComponent() { const deleteRequiredFileMutation = useDeleteRequiredFile(); - // Cột bảng - const columns: ColumnDef[] = [ - { accessorKey: "version", header: "Phiên bản" }, - { accessorKey: "fileName", header: "Tên file" }, - { - accessorKey: "updatedAt", - header: () =>
Thời gian cập nhật
, - cell: ({ getValue }) => - getValue() - ? new Date(getValue() as string).toLocaleString("vi-VN") - : "N/A", - }, - { - accessorKey: "requestUpdateAt", - header: () =>
Thời gian yêu cầu cài đặt/tải xuống
, - cell: ({ getValue }) => - getValue() - ? new Date(getValue() as string).toLocaleString("vi-VN") - : "N/A", - }, - { - id: "required", - header: () =>
Đã thêm vào danh sách
, - cell: ({ row }) => { - const isRequired = row.original.isRequired; - return isRequired ? ( -
- - -
- ) : ( -
- - Không -
- ); - }, - enableSorting: false, - enableHiding: false, - }, - { - id: "select", - header: () =>
Chọn
, - cell: ({ row }) => ( - - ), - enableSorting: false, - enableHiding: false, - }, - ]; - + const columns = useMemo( + () => createAppsColumns(installMutation.isPending), + [installMutation.isPending] +); // Upload file MSI const handleUpload = async ( fd: FormData, diff --git a/src/routes/_auth/audits/index.tsx b/src/routes/_auth/audits/index.tsx new file mode 100644 index 0000000..30f925a --- /dev/null +++ b/src/routes/_auth/audits/index.tsx @@ -0,0 +1,92 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { useState, useEffect } from "react"; +import { useGetAudits } from "@/hooks/queries"; +import type { Audits } from "@/types/audit"; +import { AuditListTemplate } from "@/template/audit-list-template"; +import { auditColumns } from "@/components/columns/audit-column"; + +export const Route = createFileRoute("/_auth/audits/")({ + head: () => ({ meta: [{ title: "Audit Logs" }] }), + loader: async ({ context }) => { + context.breadcrumbs = [{ title: "Audit logs", path: "#" }]; + }, + component: AuditsPage, +}); + +function AuditsPage() { + const [pageNumber, setPageNumber] = useState(1); + const [pageSize] = useState(20); + const [username, setUsername] = useState(null); + const [action, setAction] = useState(null); + const [from, setFrom] = useState(null); + const [to, setTo] = useState(null); + const [selectedAudit, setSelectedAudit] = useState(null); + + const { data, isLoading, refetch, isFetching } = useGetAudits( + { + pageNumber, + pageSize, + username, + action, + from, + to, + }, + true + ) as any; + + const items: Audits[] = data?.items ?? []; + const total: number = data?.totalCount ?? 0; + const pageCount = Math.max(1, Math.ceil(total / pageSize)); + + useEffect(() => { + refetch(); + }, [pageNumber, pageSize]); + + const handleSearch = () => { + setPageNumber(1); + refetch(); + }; + + const handleReset = () => { + setUsername(null); + setAction(null); + setFrom(null); + setTo(null); + setPageNumber(1); + refetch(); + }; + + return ( + 1} + canNextPage={pageNumber < pageCount} + onPreviousPage={() => setPageNumber((p) => Math.max(1, p - 1))} + onNextPage={() => setPageNumber((p) => Math.min(pageCount, p + 1))} + // filter + username={username} + action={action} + from={from} + to={to} + onUsernameChange={setUsername} + onActionChange={setAction} + onFromChange={setFrom} + onToChange={setTo} + onSearch={handleSearch} + onReset={handleReset} + // detail dialog + selectedAudit={selectedAudit} + onRowClick={setSelectedAudit} + onDialogClose={() => setSelectedAudit(null)} + /> + ); +} diff --git a/src/routes/_auth/commands/index.tsx b/src/routes/_auth/commands/index.tsx index 86950e6..ccba3e6 100644 --- a/src/routes/_auth/commands/index.tsx +++ b/src/routes/_auth/commands/index.tsx @@ -11,7 +11,6 @@ import { useSendCommand, } from "@/hooks/queries"; import { toast } from "sonner"; -import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { Check, X, Edit2, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import type { ColumnDef } from "@tanstack/react-table"; diff --git a/src/routes/_auth/rooms/$roomName/connect/index.tsx b/src/routes/_auth/rooms/$roomName/connect/index.tsx index e69de29..edd3bb7 100644 --- a/src/routes/_auth/rooms/$roomName/connect/index.tsx +++ b/src/routes/_auth/rooms/$roomName/connect/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/rooms/$roomName/connect/')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/_auth/rooms/$roomName/connect/"!
+} diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts new file mode 100644 index 0000000..3a2e88e --- /dev/null +++ b/src/services/audit.service.ts @@ -0,0 +1,19 @@ +import axios from "@/config/axios"; +import { API_ENDPOINTS } from "@/config/api"; +import type { PageResult, Audits } from "@/types/audit"; + +export async function getAudits( + pageNumber = 1, + pageSize = 20, + username?: string | null, + action?: string | null, + from?: string | null, + to?: string | null +): Promise> { + const response = await axios.get>(API_ENDPOINTS.AUDIT.GET_AUDITS, { + params: { pageNumber, pageSize, username, action, from, to }, + }); + + // API trả về camelCase khớp với PageResult — dùng trực tiếp, không cần map + return response.data; +} diff --git a/src/template/audit-list-template.tsx b/src/template/audit-list-template.tsx new file mode 100644 index 0000000..60295d5 --- /dev/null +++ b/src/template/audit-list-template.tsx @@ -0,0 +1,242 @@ +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, +} from "@/components/ui/table"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { type Audits } from "@/types/audit"; +import { AuditFilterBar } from "@/components/filters/audit-filter-bar"; +import { AuditDetailDialog } from "@/components/dialogs/audit-detail-dialog"; + +interface AuditListTemplateProps { + // data + items: Audits[]; + total: number; + columns: ColumnDef[]; + isLoading: boolean; + isFetching: boolean; + + // pagination + pageNumber: number; + pageSize: number; + pageCount: number; + onPreviousPage: () => void; + onNextPage: () => void; + canPreviousPage: boolean; + canNextPage: boolean; + + // filter + username: string | null; + action: string | null; + from: string | null; + to: string | null; + onUsernameChange: (v: string | null) => void; + onActionChange: (v: string | null) => void; + onFromChange: (v: string | null) => void; + onToChange: (v: string | null) => void; + onSearch: () => void; + onReset: () => void; + + // detail dialog + selectedAudit: Audits | null; + onRowClick: (audit: Audits) => void; + onDialogClose: () => void; +} + +export function AuditListTemplate({ + items, + total, + columns, + isLoading, + isFetching, + pageNumber, + pageSize, + pageCount, + onPreviousPage, + onNextPage, + canPreviousPage, + canNextPage, + username, + action, + from, + to, + onUsernameChange, + onActionChange, + onFromChange, + onToChange, + onSearch, + onReset, + selectedAudit, + onRowClick, + onDialogClose, +}: AuditListTemplateProps) { + const table = useReactTable({ + data: items, + columns, + state: { + pagination: { pageIndex: Math.max(0, pageNumber - 1), pageSize }, + }, + pageCount, + manualPagination: true, + onPaginationChange: (updater) => { + const next = + typeof updater === "function" + ? updater({ pageIndex: Math.max(0, pageNumber - 1), pageSize }) + : updater; + const newPage = (next.pageIndex ?? 0) + 1; + if (newPage > pageNumber) onNextPage(); + else if (newPage < pageNumber) onPreviousPage(); + }, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+
+

Nhật ký hoạt động

+

+ Xem nhật ký audit hệ thống +

+
+ + + + Danh sách audit + + Lọc theo người dùng, loại, hành động và khoảng thời gian. Nhấn vào + dòng để xem chi tiết. + + + + + + +
+ + + {table.getHeaderGroups().map((hg) => ( + + {hg.headers.map((header) => ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + + {isLoading || isFetching ? ( + + + Đang tải... + + + ) : table.getRowModel().rows.length === 0 ? ( + + + Không có dữ liệu + + + ) : ( + table.getRowModel().rows.map((row) => ( + onRowClick(row.original)} + > + {row.getVisibleCells().map((cell) => ( + + {cell.column.columnDef.cell + ? flexRender( + cell.column.columnDef.cell, + cell.getContext() + ) + : String(cell.getValue() ?? "")} + + ))} + + )) + )} + +
+
+ +
+ + Hiển thị {items.length} / {total} mục + +
+ + + {pageNumber} / {pageCount} + + +
+
+
+
+ + +
+ ); +} \ No newline at end of file diff --git a/src/types/app-sidebar.ts b/src/types/app-sidebar.ts index 8adc1eb..b8c872b 100644 --- a/src/types/app-sidebar.ts +++ b/src/types/app-sidebar.ts @@ -1,4 +1,4 @@ -import { AppWindow, Building, CircleX, Folder, Home, ShieldCheck, Terminal, UserPlus} from "lucide-react"; +import { AppWindow, Building, CircleX, ClipboardList, Folder, Home, ShieldCheck, Terminal, UserPlus} from "lucide-react"; import { PermissionEnum } from "./permission"; enum AppSidebarSectionCode { @@ -93,6 +93,17 @@ export const appSidebarSection = { permissions: [PermissionEnum.VIEW_USER], } ] + }, + { + title: "Audits", + items: [ + { + title: "Lịch sử hoạt động", + url: "/audits", + icon: ClipboardList, + permissions: [PermissionEnum.VIEW_AUDIT_LOGS], + } + ] } ], }; diff --git a/src/types/audit.ts b/src/types/audit.ts new file mode 100644 index 0000000..8dfc17c --- /dev/null +++ b/src/types/audit.ts @@ -0,0 +1,29 @@ +export interface Audits { + id: number; + username: string; + dateTime: string; // ISO string + + // request identity + apiCall?: string; // Controller.ActionName + url?: string; + requestPayload?: string; // request body (redacted) + + // DB fields — null if request didn't touch DB + action?: string; + tableName?: string; + entityId?: string; + oldValues?: string; + newValues?: string; + + // result + isSuccess?: boolean; + errorMessage?: string; +} + +export interface PageResult { + items: T[]; + totalCount: number; + pageNumber: number; + pageSize: number; + totalPages: number; +} diff --git a/src/types/permission.ts b/src/types/permission.ts index 95ddb69..c20306b 100644 --- a/src/types/permission.ts +++ b/src/types/permission.ts @@ -44,6 +44,7 @@ export enum PermissionEnum { EDIT_COMMAND = 53, DEL_COMMAND = 54, SEND_COMMAND = 55, + SEND_SENSITIVE_COMMAND = 56, //DEVICE_OPERATION DEVICE_OPERATION = 70, @@ -59,10 +60,12 @@ export enum PermissionEnum { VIEW_ACCOUNT_ROOM = 115, EDIT_ACCOUNT_ROOM = 116, + //WARNING_OPERATION WARNING_OPERATION = 140, VIEW_WARNING = 141, + //USER_OPERATION USER_OPERATION = 150, VIEW_USER_ROLE = 151, @@ -80,7 +83,7 @@ export enum PermissionEnum { DEL_ROLE = 164, // AGENT - APP_OPERATION = 170, + AGENT_OPERATION = 170, VIEW_AGENT = 171, UPDATE_AGENT = 173, SEND_UPDATE_COMMAND = 174, @@ -94,9 +97,13 @@ export enum PermissionEnum { ADD_APP_TO_SELECTED = 185, DEL_APP_FROM_SELECTED = 186, + // AUDIT + AUDIT_OPERATION = 190, + VIEW_AUDIT_LOGS = 191, + //Undefined UNDEFINED = 9999, //Allow All - ALLOW_ALL = 0, + ALLOW_ALL = 0 }