refactor code

This commit is contained in:
Do Manh Phuong 2025-12-22 14:53:19 +07:00
parent 890b27b96d
commit b0380b7c9f
48 changed files with 2785 additions and 757 deletions

341
HOOKS_USAGE_GUIDE.md Normal file
View File

@ -0,0 +1,341 @@
# TanStack Query Hooks Documentation
Tất cả các API đã được tách riêng thành TanStack Query hooks trong folder `src/hooks/queries/`.
## Cấu trúc
```
src/hooks/queries/
├── index.ts # Export tất cả hooks
├── useAuthQueries.ts # Auth hooks
├── useAppVersionQueries.ts # App/Software hooks
├── useDeviceCommQueries.ts # Device communication hooks
└── useCommandQueries.ts # Command hooks
```
## Cách Sử Dụng
### 1. Auth Queries (Xác thực)
#### Đăng nhập
```tsx
import { useLogin } from '@/hooks/queries'
function LoginPage() {
const loginMutation = useLogin()
const handleLogin = async () => {
try {
await loginMutation.mutateAsync({
username: 'user',
password: 'password'
})
// Tự động lưu token vào localStorage
} catch (error) {
console.error(error)
}
}
return (
<button
onClick={handleLogin}
disabled={loginMutation.isPending}
>
{loginMutation.isPending ? 'Đang đăng nhập...' : 'Đăng nhập'}
</button>
)
}
```
#### Đăng xuất
```tsx
import { useLogout } from '@/hooks/queries'
function LogoutButton() {
const logoutMutation = useLogout()
return (
<button onClick={() => logoutMutation.mutate()}>
Đăng xuất
</button>
)
}
```
#### Kiểm tra phiên
```tsx
import { usePing } from '@/hooks/queries'
function CheckSession() {
const { data, isLoading } = usePing(token, true)
if (isLoading) return <div>Checking...</div>
return <div>Session: {data?.message}</div>
}
```
#### Thay đổi mật khẩu
```tsx
import { useChangePassword } from '@/hooks/queries'
function ChangePasswordForm() {
const changePasswordMutation = useChangePassword()
const handleSubmit = async () => {
await changePasswordMutation.mutateAsync({
currentPassword: 'old',
newPassword: 'new'
})
}
return <button onClick={handleSubmit}>Thay đổi</button>
}
```
### 2. App Version Queries (Phần mềm/Agent)
#### Lấy danh sách agent
```tsx
import { useGetAgentVersion } from '@/hooks/queries'
function AgentList() {
const { data: agents, isLoading } = useGetAgentVersion()
if (isLoading) return <div>Loading...</div>
return <div>{agents?.length} agents</div>
}
```
#### Lấy danh sách phần mềm
```tsx
import { useGetSoftwareList } from '@/hooks/queries'
function SoftwareList() {
const { data: software, isLoading } = useGetSoftwareList()
return software?.map(item => <div key={item.id}>{item.name}</div>)
}
```
#### Upload file
```tsx
import { useUploadSoftware } from '@/hooks/queries'
function UploadForm() {
const uploadMutation = useUploadSoftware()
const handleUpload = async (file: File) => {
const formData = new FormData()
formData.append('file', file)
await uploadMutation.mutateAsync({
formData,
onUploadProgress: (event) => {
const percent = (event.loaded / event.total) * 100
console.log(`Upload: ${percent}%`)
}
})
}
return <input type="file" onChange={(e) => e.target.files && handleUpload(e.target.files[0])} />
}
```
#### Quản lý blacklist
```tsx
import {
useGetBlacklist,
useAddBlacklist,
useDeleteBlacklist
} from '@/hooks/queries'
function BlacklistManager() {
const { data: blacklist } = useGetBlacklist()
const addMutation = useAddBlacklist()
const deleteMutation = useDeleteBlacklist()
const handleAdd = async () => {
await addMutation.mutateAsync({ appId: 1 })
}
const handleDelete = async (appId: number) => {
await deleteMutation.mutateAsync(appId)
}
return (
<>
{blacklist?.map(item => (
<div key={item.id}>
{item.name}
<button onClick={() => handleDelete(item.id)}>Delete</button>
</div>
))}
<button onClick={handleAdd}>Add</button>
</>
)
}
```
### 3. Device Communication Queries
#### Lấy danh sách phòng
```tsx
import { useGetRoomList } from '@/hooks/queries'
function RoomSelector() {
const { data: rooms } = useGetRoomList()
return (
<select>
{rooms?.map(room => (
<option key={room.id} value={room.id}>{room.name}</option>
))}
</select>
)
}
```
#### Lấy thiết bị trong phòng
```tsx
import { useGetDeviceFromRoom } from '@/hooks/queries'
function DeviceList({ roomName }: { roomName: string }) {
const { data: devices, isLoading } = useGetDeviceFromRoom(roomName, true)
if (isLoading) return <div>Loading devices...</div>
return devices?.map(device => (
<div key={device.id}>{device.name}</div>
))
}
```
#### Gửi lệnh
```tsx
import { useSendCommand } from '@/hooks/queries'
function CommandForm() {
const sendMutation = useSendCommand()
const handleSend = async () => {
await sendMutation.mutateAsync({
roomName: 'Room A',
data: { command: 'dir' }
})
}
return <button onClick={handleSend}>Gửi lệnh</button>
}
```
#### Cài đặt phần mềm
```tsx
import { useInstallMsi } from '@/hooks/queries'
function InstallSoftware() {
const installMutation = useInstallMsi()
const handleInstall = async () => {
await installMutation.mutateAsync({
roomName: 'Room A',
data: { msiFileId: 1 }
})
}
return <button onClick={handleInstall}>Cài đặt</button>
}
```
### 4. Command Queries
#### Lấy danh sách lệnh
```tsx
import { useGetCommandList } from '@/hooks/queries'
function CommandList() {
const { data: commands } = useGetCommandList()
return commands?.map(cmd => <div key={cmd.id}>{cmd.name}</div>)
}
```
#### Thêm lệnh
```tsx
import { useAddCommand } from '@/hooks/queries'
function AddCommandForm() {
const addMutation = useAddCommand()
const handleAdd = async () => {
await addMutation.mutateAsync({
name: 'My Command',
command: 'echo hello'
})
}
return <button onClick={handleAdd}>Add Command</button>
}
```
## Lợi ích
1. **Automatic Caching** - TanStack Query tự động cache dữ liệu
2. **Background Refetching** - Cập nhật dữ liệu trong background
3. **Stale Time Management** - Kiểm soát thời gian dữ liệu còn "fresh"
4. **Automatic Invalidation** - Tự động update dữ liệu sau mutations
5. **Deduplication** - Gộp các request giống nhau
6. **Error Handling** - Xử lý lỗi tập trung
7. **Loading States** - Tracking loading/pending/error states
## Advanced Usage
### Dependent Queries
```tsx
function DeviceDetails({ deviceId }: { deviceId: number }) {
const { data: device } = useGetDeviceFromRoom(deviceId, true)
// Chỉ fetch khi có device
const { data: status } = useGetClientFolderStatus(
device?.roomName,
!!device
)
return <div>{status?.path}</div>
}
```
### Prefetching
```tsx
import { useQueryClient } from '@tanstack/react-query'
import { useGetSoftwareList } from '@/hooks/queries'
function PrefetchOnHover() {
const queryClient = useQueryClient()
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['app-version', 'software'],
queryFn: () => useGetSoftwareList
})
}
return <div onMouseEnter={handleMouseEnter}>Hover me</div>
}
```
## Migration từ cách cũ
**Trước:**
```tsx
const { data } = useQueryData({
queryKey: ["software-version"],
url: API_ENDPOINTS.APP_VERSION.GET_SOFTWARE,
})
```
**Sau:**
```tsx
const { data } = useGetSoftwareList()
```
Đơn giản hơn, type-safe hơn, và dễ bảo trì hơn!

174
SERVICES_GUIDE.md Normal file
View File

@ -0,0 +1,174 @@
# API Services Documentation
Tất cả logic gọi API đã được tách riêng vào folder `services`. Mỗi service tương ứng với một nhóm API.
## Cấu trúc Services
```
src/services/
├── index.ts # Export tất cả services
├── auth.service.ts # API xác thực
├── app-version.service.ts # API quản lý phần mềm
├── device-comm.service.ts # API thiết bị
├── command.service.ts # API lệnh
└── device.service.ts # Helper functions
```
## Cách Sử Dụng
### 1. Auth Service (Xác thực)
```tsx
import { authService } from '@/services'
// Đăng nhập
const response = await authService.login({
username: 'user',
password: 'pass'
})
// Đăng xuất
await authService.logout()
// Kiểm tra session
const pingResult = await authService.ping(token)
// Thay đổi mật khẩu
await authService.changePassword({
currentPassword: 'old',
newPassword: 'new'
})
// Tạo tài khoản mới (admin)
await authService.createAccount({
userName: 'newuser',
password: 'pass',
name: 'John Doe',
roleId: 1,
accessBuildings: [1, 2, 3]
})
```
### 2. App Version Service (Quản lý phần mềm)
```tsx
import { appVersionService } from '@/services'
// Lấy danh sách agent
const agents = await appVersionService.getAgentVersion()
// Lấy danh sách phần mềm
const software = await appVersionService.getSoftwareList()
// Upload file
const formData = new FormData()
formData.append('file', fileInput.files[0])
await appVersionService.uploadSoftware(formData, (progressEvent) => {
console.log(`Progress: ${progressEvent.loaded}/${progressEvent.total}`)
})
// Lấy blacklist
const blacklist = await appVersionService.getBlacklist()
// Thêm vào blacklist
await appVersionService.addBlacklist({ appId: 1, reason: 'virus' })
// Xóa khỏi blacklist
await appVersionService.deleteBlacklist(1)
```
### 3. Device Comm Service (Thiết bị)
```tsx
import { deviceCommService } from '@/services'
// Lấy tất cả thiết bị
const allDevices = await deviceCommService.getAllDevices()
// Lấy danh sách phòng
const rooms = await deviceCommService.getRoomList()
// Lấy thiết bị trong phòng
const devices = await deviceCommService.getDeviceFromRoom('Room A')
// Gửi lệnh
await deviceCommService.sendCommand('Room A', {
command: 'dir'
})
// Cập nhật agent
await deviceCommService.updateAgent('Room A', { version: '1.0.0' })
// Cài đặt MSI
await deviceCommService.installMsi('Room A', { msiFileId: 1 })
```
### 4. Command Service (Lệnh)
```tsx
import { commandService } from '@/services'
// Lấy danh sách lệnh
const commands = await commandService.getCommandList()
// Thêm lệnh
await commandService.addCommand({ name: 'cmd1', command: 'echo hello' })
// Cập nhật lệnh
await commandService.updateCommand(1, { name: 'cmd1 updated' })
// Xóa lệnh
await commandService.deleteCommand(1)
```
## Sử dụng với React Query/Hooks
### Cách cũ (trực tiếp gọi từ component):
```tsx
const { data } = useQueryData({
queryKey: ["software-version"],
url: API_ENDPOINTS.APP_VERSION.GET_SOFTWARE,
})
```
### Cách mới (tách biệt logic):
Có thể tạo custom hooks bao quanh services:
```tsx
// hooks/useGetSoftware.ts
import { useQuery } from '@tanstack/react-query'
import { appVersionService } from '@/services'
export function useGetSoftware() {
return useQuery({
queryKey: ['software-version'],
queryFn: () => appVersionService.getSoftwareList(),
staleTime: 60 * 1000,
})
}
```
Sau đó sử dụng trong component:
```tsx
function AppsComponent() {
const { data, isLoading } = useGetSoftware()
// ...
}
```
## Lợi ích của cách sử dụng mới
1. **Tách biệt logic** - Logic API nằm riêng, dễ bảo trì
2. **Tái sử dụng** - Có thể sử dụng service từ bất kỳ nơi
3. **Dễ test** - Dễ mock services khi viết unit tests
4. **Centralized error handling** - Có thể xử lý lỗi chung
5. **Type safety** - TypeScript types cho tất cả API requests/responses
## Cải tiến trong tương lai
Có thể thêm:
- Global error handling middleware trong axios
- Request/response interceptors cho authentication
- Retry logic cho failed requests
- Request cancellation

328
SERVICES_VS_HOOKS.md Normal file
View File

@ -0,0 +1,328 @@
# Khác biệt giữa Services và Query Hooks
## Tóm tắt nhanh
| Aspect | Services | Query Hooks |
|--------|----------|-------------|
| **Location** | `src/services/` | `src/hooks/queries/` |
| **Mục đích** | Gọi API trực tiếp | Wrapper TanStack Query |
| **Caching** | ❌ Không | ✅ Có |
| **Background Refetch** | ❌ Không | ✅ Có |
| **Auto Invalidation** | ❌ Không | ✅ Có |
| **Type** | Async functions | React Hooks |
| **Dùng trong** | Non-React code, utilities | React components |
---
## Chi tiết Từng Layer
### 1. Services Layer (`src/services/`)
**Mục đích:** Đơn thuần gọi API và trả về dữ liệu
```typescript
// app-version.service.ts
export async function getSoftwareList(): Promise<Version[]> {
const response = await axios.get<Version[]>(
API_ENDPOINTS.APP_VERSION.GET_SOFTWARE
);
return response.data;
}
```
**Đặc điểm:**
- ✅ Pure async functions
- ✅ Không phụ thuộc vào React
- ✅ Có thể sử dụng ở bất kỳ đâu (utils, servers, non-React code)
- ❌ Không có caching
- ❌ Phải tự quản lý state loading/error
- ❌ Phải tự gọi lại khi dữ liệu thay đổi
**Khi nào dùng:**
```typescript
// Dùng trong utility functions
export async function initializeApp() {
const software = await appVersionService.getSoftwareList();
// ...
}
// Hoặc trong services khác
export async function validateSoftware() {
const list = await appVersionService.getSoftwareList();
// ...
}
```
---
### 2. Query Hooks Layer (`src/hooks/queries/`)
**Mục đích:** Wrapper TanStack Query bên trên services
```typescript
// useAppVersionQueries.ts
export function useGetSoftwareList(enabled = true) {
return useQuery<Version[]>({
queryKey: ["app-version", "software"],
queryFn: () => appVersionService.getSoftwareList(),
enabled,
staleTime: 60 * 1000, // 1 minute
});
}
```
**Đặc điểm:**
- ✅ React hooks
- ✅ Automatic caching
- ✅ Background refetching
- ✅ Automatic invalidation sau mutations
- ✅ Built-in loading/error states
- ✅ Deduplication (gộp requests giống nhau)
- ❌ Chỉ dùng được trong React components
- ❌ Phức tạp hơn services
**Khi nào dùng:**
```typescript
// Dùng trong React components
function SoftwareList() {
const { data: software, isLoading } = useGetSoftwareList()
if (isLoading) return <div>Loading...</div>
return software?.map(item => <div>{item.name}</div>)
}
```
---
## So sánh cụ thể
### Ví dụ 1: Lấy danh sách
**Service - Raw API call:**
```typescript
// services/app-version.service.ts
export async function getSoftwareList(): Promise<Version[]> {
const response = await axios.get(API_ENDPOINTS.APP_VERSION.GET_SOFTWARE);
return response.data;
}
```
**Hook - TanStack Query wrapper:**
```typescript
// hooks/queries/useAppVersionQueries.ts
export function useGetSoftwareList(enabled = true) {
return useQuery({
queryKey: ["app-version", "software"],
queryFn: () => appVersionService.getSoftwareList(),
staleTime: 60 * 1000,
});
}
```
**Sử dụng trong component:**
```typescript
function Component() {
// ❌ KHÔNG nên dùng service trực tiếp
const [data, setData] = useState<Version[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
appVersionService.getSoftwareList().then(d => {
setData(d);
setLoading(false);
});
}, []);
// ✅ NÊN dùng hook thay vì
const { data, isLoading } = useGetSoftwareList();
}
```
---
### Ví dụ 2: Upload file
**Service:**
```typescript
// services/app-version.service.ts
export async function uploadSoftware(
formData: FormData,
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
): Promise<{ message: string }> {
return axios.post(API_ENDPOINTS.APP_VERSION.UPLOAD, formData, {
onUploadProgress,
});
}
```
**Hook:**
```typescript
// hooks/queries/useAppVersionQueries.ts
export function useUploadSoftware() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { formData: FormData; onUploadProgress?: ... }) =>
appVersionService.uploadSoftware(data.formData, data.onUploadProgress),
onSuccess: () => {
// Tự động invalidate software list
queryClient.invalidateQueries({
queryKey: ["app-version", "software"],
});
},
});
}
```
**Sử dụng:**
```typescript
function UploadForm() {
const uploadMutation = useUploadSoftware();
const handleUpload = async (file: File) => {
const formData = new FormData();
formData.append("file", file);
await uploadMutation.mutateAsync({
formData,
onUploadProgress: (e) => console.log(`${e.loaded}/${e.total}`)
});
// ✅ Software list tự động update
};
return (
<button disabled={uploadMutation.isPending}>
{uploadMutation.isPending ? "Uploading..." : "Upload"}
</button>
);
}
```
---
## Architecture Flow
```
┌─────────────────────────────────────────┐
│ React Component │
│ (SoftwareList, UploadForm, etc.) │
└──────────────┬──────────────────────────┘
│ uses
┌─────────────────────────────────────────┐
│ Query Hooks (src/hooks/queries/) │
│ - useGetSoftwareList() │
│ - useUploadSoftware() │
│ - useDeleteBlacklist() │
│ - Features: │
│ - Caching │
│ - Auto invalidation │
│ - Loading states │
└──────────────┬──────────────────────────┘
│ wraps
┌─────────────────────────────────────────┐
│ Service Functions (src/services/) │
│ - getSoftwareList() │
│ - uploadSoftware() │
│ - deleteBlacklist() │
│ - Features: │
│ - Pure async functions │
│ - Direct API calls │
└──────────────┬──────────────────────────┘
│ uses
┌─────────────────────────────────────────┐
│ Axios (HTTP Client) │
└──────────────┬──────────────────────────┘
│ requests
┌─────────────────────────────────────────┐
│ Backend API Server │
└─────────────────────────────────────────┘
```
---
## Nguyên tắc sử dụng
### ✅ NÊN dùng Services khi:
- Gọi API từ non-React code (utilities, event handlers, etc.)
- Cần gọi API một lần rồi không cần tracking state
- Không cần caching hay background refetch
- Viết code không phụ thuộc React
```typescript
// ✅ OK - utility function
export async function syncData() {
const software = await appVersionService.getSoftwareList();
const commands = await commandService.getCommandList();
return { software, commands };
}
```
### ✅ NÊN dùng Hooks khi:
- Lấy/update dữ liệu trong React components
- Cần caching và background refetch
- Muốn dữ liệu tự động update
- Cần tracking loading/error states
```typescript
// ✅ OK - React component
function Dashboard() {
const { data: software, isLoading } = useGetSoftwareList();
const uploadMutation = useUploadSoftware();
return <div>{/* ... */}</div>;
}
```
### ❌ KHÔNG nên dùng Services khi:
- Đang trong React component và cần state management
- Cần automatic refetching
- Cần auto-invalidation sau mutations
```typescript
// ❌ WRONG
function Component() {
const [data, setData] = useState([]);
useEffect(() => {
appVersionService.getSoftwareList().then(setData);
}, []);
}
// ✅ RIGHT
function Component() {
const { data } = useGetSoftwareList();
}
```
### ❌ KHÔNG nên dùng Hooks khi:
- Không phải trong React component
- Không có React context
```typescript
// ❌ WRONG - không thể gọi hooks ở đây
export function initApp() {
const { data } = useGetSoftwareList(); // ERROR!
}
// ✅ RIGHT
export async function initApp() {
const data = await appVersionService.getSoftwareList();
}
```
---
## Summary
**Services** = Cơ sở API calls, có thể tái sử dụng ở bất kỳ đâu
**Hooks** = Lớp React trên services, tối ưu cho React components
**Dùng Services** khi bạn cần tính linh hoạt và độc lập với React
**Dùng Hooks** khi bạn muốn TanStack Query quản lý state và caching tự động

View File

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,24 @@
import { AlertTriangle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Link } from "@tanstack/react-router";
export default function ErrorRoute({ error }: { error: string }) {
return (
<div className="flex flex-col items-center justify-center min-h-screen px-4 text-center">
<div className="bg-destructive/10 rounded-full p-6 mb-6">
<AlertTriangle className="h-12 w-12 text-destructive" />
</div>
<h1 className="text-4xl font-bold mb-4">Lỗi</h1>
<p className="text-muted-foreground mb-8 max-w-md">
Đã xảy ra lỗi: <strong>{error}</strong>
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Button asChild variant="outline">
<Link to="/dashboard">Về trang chủ</Link>
</Button>
</div>
</div>
);
}

View File

@ -0,0 +1,35 @@
import { Button } from "@/components/ui/button";
import { Link } from "@tanstack/react-router";
import { ArrowLeft, Search } from "lucide-react";
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-[70vh] px-4 text-center">
<div className="space-y-6 max-w-md mx-auto">
<div className="relative mx-auto w-40 h-40 md:w-52 md:h-52">
<div className="absolute inset-0 bg-primary/10 rounded-full animate-pulse" />
<div className="absolute inset-0 flex items-center justify-center">
<Search className="h-20 w-20 md:h-24 md:w-24 text-primary" strokeWidth={1.5} />
</div>
</div>
<div className="space-y-3">
<h1 className="text-4xl md:text-5xl font-bold tracking-tighter">404</h1>
<h2 className="text-2xl md:text-3xl font-semibold">Không tìm thấy</h2>
<p className="text-muted-foreground">
Trang bạn yêu cầu truy cập không tồn tại hoặc đã bị xoá.
</p>
</div>
<div className="pt-6">
<Button asChild size="lg" className="gap-2">
<Link to="/dashboard" className="flex items-center gap-1">
<ArrowLeft className="h-4 w-4" />
Trở về trang chủ
</Link>
</Button>
</div>
</div>
</div>
);
}

View File

@ -5,6 +5,15 @@ export const BASE_URL = isDev
: "/api"; : "/api";
export const API_ENDPOINTS = { export const API_ENDPOINTS = {
AUTH: {
LOGIN: `${BASE_URL}/login`,
LOGOUT: `${BASE_URL}/logout`,
CHANGE_PASSWORD: `${BASE_URL}/auth/change-password`,
CHANGE_PASSWORD_ADMIN: `${BASE_URL}/auth/admin/change-password`,
PING: `${BASE_URL}/ping`,
CSRF_TOKEN: `${BASE_URL}/csrf-token`,
CREATE_ACCOUNT: `${BASE_URL}/auth/create-account`,
},
APP_VERSION: { APP_VERSION: {
//agent and app api //agent and app api
GET_VERSION: `${BASE_URL}/AppVersion/version`, GET_VERSION: `${BASE_URL}/AppVersion/version`,

View File

@ -0,0 +1,11 @@
// Auth Queries
export * from "./useAuthQueries";
// App Version Queries
export * from "./useAppVersionQueries";
// Device Communication Queries
export * from "./useDeviceCommQueries";
// Command Queries
export * from "./useCommandQueries";

View File

@ -0,0 +1,186 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as appVersionService from "@/services/app-version.service";
import type { AxiosProgressEvent } from "axios";
import type { Version } from "@/types/file";
const APP_VERSION_QUERY_KEYS = {
all: ["app-version"] as const,
agentVersion: () => [...APP_VERSION_QUERY_KEYS.all, "agent"] as const,
softwareList: () => [...APP_VERSION_QUERY_KEYS.all, "software"] as const,
blacklist: () => [...APP_VERSION_QUERY_KEYS.all, "blacklist"] as const,
requiredFiles: () => [...APP_VERSION_QUERY_KEYS.all, "required-files"] as const,
};
/**
* Hook đ lấy danh sách phiên bản agent
*/
export function useGetAgentVersion(enabled = true) {
return useQuery<Version[]>({
queryKey: APP_VERSION_QUERY_KEYS.agentVersion(),
queryFn: () => appVersionService.getAgentVersion(),
enabled,
staleTime: 60 * 1000, // 1 minute
});
}
/**
* Hook đ lấy danh sách phần mềm
*/
export function useGetSoftwareList(enabled = true) {
return useQuery<Version[]>({
queryKey: APP_VERSION_QUERY_KEYS.softwareList(),
queryFn: () => appVersionService.getSoftwareList(),
enabled,
staleTime: 60 * 1000,
});
}
/**
* Hook đ upload file
*/
export function useUploadSoftware() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: {
formData: FormData;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
}) => appVersionService.uploadSoftware(data.formData, data.onUploadProgress),
onSuccess: () => {
// Invalidate software list
queryClient.invalidateQueries({
queryKey: APP_VERSION_QUERY_KEYS.softwareList(),
});
},
});
}
/**
* Hook đ lấy danh sách blacklist
*/
export function useGetBlacklist(enabled = true) {
return useQuery({
queryKey: APP_VERSION_QUERY_KEYS.blacklist(),
queryFn: () => appVersionService.getBlacklist(),
enabled,
staleTime: 60 * 1000,
});
}
/**
* Hook đ thêm vào blacklist
*/
export function useAddBlacklist() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => appVersionService.addBlacklist(data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: APP_VERSION_QUERY_KEYS.blacklist(),
});
},
});
}
/**
* Hook đ xóa khỏi blacklist
*/
export function useDeleteBlacklist() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (appId: number) => appVersionService.deleteBlacklist(appId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: APP_VERSION_QUERY_KEYS.blacklist(),
});
},
});
}
/**
* Hook đ cập nhật blacklist
*/
export function useUpdateBlacklist() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ appId, data }: { appId: string; data: any }) =>
appVersionService.updateBlacklist(appId, data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: APP_VERSION_QUERY_KEYS.blacklist(),
});
},
});
}
/**
* Hook đ yêu cầu cập nhật blacklist
*/
export function useRequestUpdateBlacklist() {
return useMutation({
mutationFn: (data: any) => appVersionService.requestUpdateBlacklist(data),
});
}
/**
* Hook đ lấy danh sách file bắt buộc
*/
export function useGetRequiredFiles(enabled = true) {
return useQuery({
queryKey: APP_VERSION_QUERY_KEYS.requiredFiles(),
queryFn: () => appVersionService.getRequiredFiles(),
enabled,
staleTime: 60 * 1000,
});
}
/**
* Hook đ thêm file bắt buộc
*/
export function useAddRequiredFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => appVersionService.addRequiredFile(data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: APP_VERSION_QUERY_KEYS.requiredFiles(),
});
},
});
}
/**
* Hook đ xóa file bắt buộc
*/
export function useDeleteRequiredFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (fileId: number) => appVersionService.deleteRequiredFile(fileId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: APP_VERSION_QUERY_KEYS.requiredFiles(),
});
},
});
}
/**
* Hook đ xóa file
*/
export function useDeleteFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (fileId: number) => appVersionService.deleteFile(fileId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: APP_VERSION_QUERY_KEYS.softwareList(),
});
},
});
}

View File

@ -0,0 +1,120 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as authService from "@/services/auth.service";
import type { LoginResquest, LoginResponse } from "@/types/auth";
const AUTH_QUERY_KEYS = {
all: ["auth"] as const,
ping: () => [...AUTH_QUERY_KEYS.all, "ping"] as const,
csrfToken: () => [...AUTH_QUERY_KEYS.all, "csrf-token"] as const,
};
/**
* Hook đ đăng nhập
*/
export function useLogin() {
const queryClient = useQueryClient();
return useMutation<LoginResponse, any, LoginResquest>({
mutationFn: (credentials) => authService.login(credentials),
onSuccess: (data) => {
// Lưu vào localStorage
if (data.token) {
localStorage.setItem("token", data.token);
localStorage.setItem("username", data.username || "");
localStorage.setItem("name", data.name || "");
localStorage.setItem("acs", JSON.stringify(data.access || []));
localStorage.setItem("role", data.role?.roleName || "");
localStorage.setItem("priority", String(data.role?.priority || "-1"));
}
// Invalidate ping query
queryClient.invalidateQueries({ queryKey: AUTH_QUERY_KEYS.ping() });
},
});
}
/**
* Hook đ đăng xuất
*/
export function useLogout() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => authService.logout(),
onSuccess: () => {
localStorage.removeItem("token");
localStorage.removeItem("username");
localStorage.removeItem("name");
localStorage.removeItem("acs");
localStorage.removeItem("role");
localStorage.removeItem("priority");
// Clear all queries
queryClient.clear();
},
});
}
/**
* Hook đ kiểm tra phiên đăng nhập
*/
export function usePing(token?: string, enabled = true) {
return useQuery({
queryKey: AUTH_QUERY_KEYS.ping(),
queryFn: () => authService.ping(token),
enabled,
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 1,
});
}
/**
* Hook đ lấy CSRF token
*/
export function useGetCsrfToken(enabled = true) {
return useQuery({
queryKey: AUTH_QUERY_KEYS.csrfToken(),
queryFn: () => authService.getCsrfToken(),
enabled,
staleTime: Infinity,
});
}
/**
* Hook đ thay đi mật khẩu
*/
export function useChangePassword() {
return useMutation({
mutationFn: (data: { currentPassword: string; newPassword: string }) =>
authService.changePassword(data),
});
}
/**
* Hook đ admin thay đi mật khẩu user khác
*/
export function useChangePasswordAdmin() {
return useMutation({
mutationFn: (data: { username: string; newPassword: string }) =>
authService.changePasswordAdmin(data),
});
}
/**
* Hook đ tạo tài khoản mới
*/
export function useCreateAccount() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: {
userName: string;
password: string;
name: string;
roleId: number;
accessBuildings?: number[];
}) => authService.createAccount(data),
onSuccess: () => {
// Có thể invalidate user list query nếu có
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
}

View File

@ -0,0 +1,74 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as commandService from "@/services/command.service";
const COMMAND_QUERY_KEYS = {
all: ["commands"] as const,
list: () => [...COMMAND_QUERY_KEYS.all, "list"] as const,
detail: (id: number) => [...COMMAND_QUERY_KEYS.all, "detail", id] as const,
};
/**
* Hook đ lấy danh sách lệnh
*/
export function useGetCommandList(enabled = true) {
return useQuery({
queryKey: COMMAND_QUERY_KEYS.list(),
queryFn: () => commandService.getCommandList(),
enabled,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
/**
* Hook đ thêm lệnh mới
*/
export function useAddCommand() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => commandService.addCommand(data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: COMMAND_QUERY_KEYS.list(),
});
},
});
}
/**
* Hook đ cập nhật lệnh
*/
export function useUpdateCommand() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
commandId,
data,
}: {
commandId: number;
data: any;
}) => commandService.updateCommand(commandId, data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: COMMAND_QUERY_KEYS.list(),
});
},
});
}
/**
* Hook đ xóa lệnh
*/
export function useDeleteCommand() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (commandId: number) => commandService.deleteCommand(commandId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: COMMAND_QUERY_KEYS.list(),
});
},
});
}

View File

@ -0,0 +1,172 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as deviceCommService from "@/services/device-comm.service";
import type { DeviceHealthCheck } from "@/types/device";
const DEVICE_COMM_QUERY_KEYS = {
all: ["device-comm"] as const,
allDevices: () => [...DEVICE_COMM_QUERY_KEYS.all, "all"] as const,
roomList: () => [...DEVICE_COMM_QUERY_KEYS.all, "rooms"] as const,
devicesInRoom: (roomName: string) =>
[...DEVICE_COMM_QUERY_KEYS.all, "room", roomName] as const,
clientFolderStatus: (roomName: string) =>
[...DEVICE_COMM_QUERY_KEYS.all, "folder-status", roomName] as const,
};
/**
* Hook đ lấy tất cả thiết bị
*/
export function useGetAllDevices(enabled = true) {
return useQuery({
queryKey: DEVICE_COMM_QUERY_KEYS.allDevices(),
queryFn: () => deviceCommService.getAllDevices(),
enabled,
staleTime: 60 * 1000,
});
}
/**
* Hook đ lấy danh sách phòng
*/
export function useGetRoomList(enabled = true) {
return useQuery({
queryKey: DEVICE_COMM_QUERY_KEYS.roomList(),
queryFn: () => deviceCommService.getRoomList(),
enabled,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
/**
* Hook đ lấy danh sách thiết bị trong phòng
*/
export function useGetDeviceFromRoom(roomName?: string, enabled = true) {
return useQuery<DeviceHealthCheck[]>({
queryKey: roomName ? DEVICE_COMM_QUERY_KEYS.devicesInRoom(roomName) : ["disabled"],
queryFn: () =>
roomName ? deviceCommService.getDeviceFromRoom(roomName) : Promise.reject("No room"),
enabled: enabled && !!roomName,
staleTime: 30 * 1000, // 30 seconds
});
}
/**
* Hook đ tải file
*/
export function useDownloadFiles() {
return useMutation({
mutationFn: ({
roomName,
data,
}: {
roomName: string;
data: any;
}) => deviceCommService.downloadFiles(roomName, data),
});
}
/**
* Hook đ cài đt MSI
*/
export function useInstallMsi() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
roomName,
data,
}: {
roomName: string;
data: any;
}) => deviceCommService.installMsi(roomName, data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: DEVICE_COMM_QUERY_KEYS.all,
});
},
});
}
/**
* Hook đ cập nhật agent
*/
export function useUpdateAgent() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
roomName,
data,
}: {
roomName: string;
data: any;
}) => deviceCommService.updateAgent(roomName, data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: DEVICE_COMM_QUERY_KEYS.all,
});
},
});
}
/**
* Hook đ cập nhật blacklist
*/
export function useUpdateDeviceBlacklist() {
return useMutation({
mutationFn: ({
roomName,
data,
}: {
roomName: string;
data: any;
}) => deviceCommService.updateBlacklist(roomName, data),
});
}
/**
* Hook đ gửi lệnh shell
*/
export function useSendCommand() {
return useMutation({
mutationFn: ({
roomName,
data,
}: {
roomName: string;
data: any;
}) => deviceCommService.sendCommand(roomName, data),
});
}
/**
* Hook đ thay đi phòng của thiết bị
*/
export function useChangeDeviceRoom() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => deviceCommService.changeDeviceRoom(data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: DEVICE_COMM_QUERY_KEYS.all,
});
},
});
}
/**
* Hook đ lấy trạng thái folder client
*/
export function useGetClientFolderStatus(roomName?: string, enabled = true) {
return useQuery({
queryKey: roomName
? DEVICE_COMM_QUERY_KEYS.clientFolderStatus(roomName)
: ["disabled"],
queryFn: () =>
roomName
? deviceCommService.getClientFolderStatus(roomName)
: Promise.reject("No room"),
enabled: enabled && !!roomName,
staleTime: 30 * 1000,
});
}

106
src/hooks/useAuth.tsx Normal file
View File

@ -0,0 +1,106 @@
import { sleep } from "@/lib/utils";
import { PermissionEnum } from "@/types/permission";
import React, { useContext, useEffect, useMemo } from "react";
import { useCallback, useState } from "react";
export interface IAuthContext {
isAuthenticated: boolean;
setAuthenticated: (value: boolean) => void;
logout: () => Promise<void>;
login: (username: string) => void;
username: string;
token: string;
name: string;
acs: number[];
hasPermission: (permission: PermissionEnum) => boolean;
role: {
roleName: string;
priority: number;
};
}
const AuthContext = React.createContext<IAuthContext | null>(null);
const key = "accesscontrol.auth.user";
function getStoredUser() {
return localStorage.getItem(key);
}
function setStoredUser(user: string | null) {
if (user) {
localStorage.setItem(key, user);
} else {
localStorage.removeItem(key);
}
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<string>(getStoredUser() || "");
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(!!user);
const token = localStorage.getItem("token") || "";
const name = localStorage.getItem("name") || "";
const acsString = localStorage.getItem("acs");
const acs = useMemo(() => (acsString ? acsString.split(",").map(Number) : []), [acsString]);
const roleName = localStorage.getItem("role") || "";
const priority = localStorage.getItem("priority") || "-1";
const setAuthenticated = useCallback((value: boolean) => {
setIsAuthenticated(value);
}, []);
const login = useCallback((username: string) => {
setStoredUser(username);
setUser(username);
}, []);
const hasPermission = useCallback(
(permission: PermissionEnum) => {
return acs.some((a) => a === permission);
},
[acs]
);
const logout = useCallback(async () => {
await sleep(250);
setAuthenticated(false);
setStoredUser("");
setUser("");
localStorage.removeItem("token");
localStorage.removeItem("name");
localStorage.removeItem("acs");
localStorage.removeItem("role");
localStorage.removeItem("priority");
}, [setAuthenticated]);
useEffect(() => {
setUser(getStoredUser() || "");
}, []);
return (
<AuthContext.Provider
value={{
isAuthenticated,
setAuthenticated,
login,
logout,
username: user,
token,
name,
acs,
role: { roleName, priority: Number(priority) },
hasPermission
}}>
{children}
</AuthContext.Provider>
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}

View File

@ -1,37 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
type DeleteDataOptions<TOutput> = {
onSuccess?: (data: TOutput) => void;
onError?: (error: any) => void;
invalidate?: string[][];
};
export function useDeleteData<TOutput = any>({
onSuccess,
onError,
invalidate = [],
}: DeleteDataOptions<TOutput> = {}) {
const queryClient = useQueryClient();
return useMutation<
TOutput,
any,
{
url: string;
config?: any;
}
>({
mutationFn: async ({ url, config }) => {
const response = await axios.delete(url, config);
return response.data;
},
onSuccess: (data) => {
invalidate.forEach((key) =>
queryClient.invalidateQueries({ queryKey: key })
);
onSuccess?.(data);
},
onError,
});
}

View File

@ -1,57 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import axios, { type Method } from "axios";
type MutationDataOptions<TInput, TOutput> = {
url: string;
method?: Method;
onSuccess?: (data: TOutput) => void;
onError?: (error: any) => void;
invalidate?: string[][];
};
export function useMutationData<TInput = any, TOutput = any>({
url,
method = "POST",
onSuccess,
onError,
invalidate = [],
}: MutationDataOptions<TInput, TOutput>) {
const queryClient = useQueryClient();
return useMutation<
TOutput,
any,
{ data: TInput; url?: string; config?: any; method?: Method }
>({
mutationFn: async ({
data,
config,
url: customUrl,
method: customMethod,
}) => {
const isFormData = data instanceof FormData;
const response = await axios.request({
url: customUrl ?? url,
method: customMethod ?? method,
data,
headers: {
...(isFormData ? {} : { "Content-Type": "application/json" }),
},
...config,
});
return response.data;
},
onSuccess: async (data) => {
// Invalidate queries trước
await Promise.all(
invalidate.map((key) =>
queryClient.invalidateQueries({ queryKey: key })
)
);
// Sau đó gọi callback
onSuccess?.(data);
},
onError,
});
}

View File

@ -1,25 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
type QueryDataOptions<T> = {
queryKey: string[];
url: string;
params?: Record<string, any>;
select?: (data: any) => T;
enabled?: boolean;
};
export function useQueryData<T = any>({
queryKey,
url,
params,
select,
enabled = true,
}: QueryDataOptions<T>) {
return useQuery<T>({
queryKey,
queryFn: () => axios.get(url, { params }).then((res) => res.data),
select,
enabled,
});
}

View File

@ -8,8 +8,13 @@ import {
import { Home, Building, AppWindow, Terminal, CircleX } from "lucide-react"; import { Home, Building, AppWindow, Terminal, CircleX } from "lucide-react";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { API_ENDPOINTS } from "@/config/api";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import {
useGetAgentVersion,
useGetSoftwareList,
useGetRoomList,
useGetBlacklist,
} from "@/hooks/queries";
type AppLayoutProps = { type AppLayoutProps = {
children: ReactNode; children: ReactNode;
@ -20,43 +25,32 @@ export default function AppLayout({ children }: AppLayoutProps) {
const handlePrefetchAgents = () => { const handlePrefetchAgents = () => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["agent-version"], queryKey: ["app-version", "agent"],
queryFn: () => queryFn: useGetAgentVersion as any,
fetch(API_ENDPOINTS.APP_VERSION.GET_VERSION).then((res) =>
res.json()
),
staleTime: 60 * 1000, staleTime: 60 * 1000,
}); });
}; };
const handlePrefetchSofware = () => { const handlePrefetchSofware = () => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["software-version"], queryKey: ["app-version", "software"],
queryFn: () => queryFn: useGetSoftwareList as any,
fetch(API_ENDPOINTS.APP_VERSION.GET_SOFTWARE).then((res) =>
res.json()
),
staleTime: 60 * 1000, staleTime: 60 * 1000,
}); });
}; };
const handlePrefetchRooms = () => { const handlePrefetchRooms = () => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["room-list"], queryKey: ["device-comm", "rooms"],
queryFn: () => queryFn: useGetRoomList as any,
fetch(API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST).then((res) => staleTime: 5 * 60 * 1000,
res.json()
),
staleTime: 60 * 1000,
}); });
}; };
const handlePrefetchBannedSoftware = () => { const handlePrefetchBannedSoftware = () => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["blacklist"], queryKey: ["app-version", "blacklist"],
queryFn: () => queryFn: useGetBlacklist as any,
fetch(API_ENDPOINTS.APP_VERSION + "").then((res) =>
res.json()
),
staleTime: 60 * 1000, staleTime: 60 * 1000,
}); });
}; };

View File

@ -4,3 +4,7 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}

View File

@ -1,25 +1,24 @@
/* eslint-disable react-refresh/only-export-components */
import { StrictMode } from "react"; import { StrictMode } from "react";
import "./index.css";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import useAuthToken from "./hooks/useAuthtoken";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// Import the generated route tree // Import the generated route tree
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./styles.css"; import axios from "axios";
import { AuthProvider, useAuth } from "@/hooks/useAuth";
const auth = useAuthToken.getState(); import { toast, Toaster } from "sonner";
export const queryClient = new QueryClient();
// Create a new router instance // Create a new router instance
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
context: { auth },
defaultPreload: "intent", defaultPreload: "intent",
scrollRestoration: true, scrollRestoration: true,
defaultStructuralSharing: true, context: {
defaultPreloadStaleTime: 0, auth: undefined!, // This will be set after we initialize the auth store
queryClient: undefined!
}
}); });
// Register the router instance for type safety // Register the router instance for type safety
@ -27,18 +26,61 @@ declare module "@tanstack/react-router" {
interface Register { interface Register {
router: typeof router; router: typeof router;
} }
interface HistoryState {
name?: string;
}
}
function InnerApp() {
const auth = useAuth();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error: unknown) => {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
if (error.response?.data.message != null) {
toast.error("Không có quyền truy cập!");
return false;
} else {
auth.logout();
queryClient.invalidateQueries();
queryClient.clear();
return false;
}
}
}
return failureCount < 3;
}
}
}
});
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} context={{ auth, queryClient }} />
</QueryClientProvider>
);
}
function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
);
} }
// Render the app // Render the app
const rootElement = document.getElementById("app"); const rootElement = document.getElementById("app");
if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement); if (!rootElement) {
root.render( throw new Error("Failed to find the root element");
<StrictMode>
<QueryClientProvider client={queryClient}>
{" "}
<RouterProvider router={router} />
</QueryClientProvider>
</StrictMode>
);
} }
const root = ReactDOM.createRoot(rootElement);
root.render(
<StrictMode>
<App />
<Toaster richColors />
</StrictMode>
);

View File

@ -9,22 +9,18 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
import { Route as AuthRouteImport } from './routes/_auth' import { Route as AuthRouteImport } from './routes/_auth'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as AuthenticatedRoomIndexRouteImport } from './routes/_authenticated/room/index' import { Route as AuthRoomIndexRouteImport } from './routes/_auth/room/index'
import { Route as AuthenticatedDeviceIndexRouteImport } from './routes/_authenticated/device/index' import { Route as AuthDeviceIndexRouteImport } from './routes/_auth/device/index'
import { Route as AuthenticatedCommandIndexRouteImport } from './routes/_authenticated/command/index' import { Route as AuthDashboardIndexRouteImport } from './routes/_auth/dashboard/index'
import { Route as AuthenticatedBlacklistIndexRouteImport } from './routes/_authenticated/blacklist/index' import { Route as AuthCommandIndexRouteImport } from './routes/_auth/command/index'
import { Route as AuthenticatedAppsIndexRouteImport } from './routes/_authenticated/apps/index' import { Route as AuthBlacklistIndexRouteImport } from './routes/_auth/blacklist/index'
import { Route as AuthenticatedAgentIndexRouteImport } from './routes/_authenticated/agent/index' import { Route as AuthAppsIndexRouteImport } from './routes/_auth/apps/index'
import { Route as AuthLoginIndexRouteImport } from './routes/_auth/login/index' import { Route as AuthAgentIndexRouteImport } from './routes/_auth/agent/index'
import { Route as AuthenticatedRoomRoomNameIndexRouteImport } from './routes/_authenticated/room/$roomName/index' import { Route as authLoginIndexRouteImport } from './routes/(auth)/login/index'
import { Route as AuthRoomRoomNameIndexRouteImport } from './routes/_auth/room/$roomName/index'
const AuthenticatedRoute = AuthenticatedRouteImport.update({
id: '/_authenticated',
getParentRoute: () => rootRouteImport,
} as any)
const AuthRoute = AuthRouteImport.update({ const AuthRoute = AuthRouteImport.update({
id: '/_auth', id: '/_auth',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
@ -34,86 +30,89 @@ const IndexRoute = IndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AuthenticatedRoomIndexRoute = AuthenticatedRoomIndexRouteImport.update({ const AuthRoomIndexRoute = AuthRoomIndexRouteImport.update({
id: '/room/', id: '/room/',
path: '/room/', path: '/room/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedDeviceIndexRoute =
AuthenticatedDeviceIndexRouteImport.update({
id: '/device/',
path: '/device/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedCommandIndexRoute =
AuthenticatedCommandIndexRouteImport.update({
id: '/command/',
path: '/command/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedBlacklistIndexRoute =
AuthenticatedBlacklistIndexRouteImport.update({
id: '/blacklist/',
path: '/blacklist/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedAppsIndexRoute = AuthenticatedAppsIndexRouteImport.update({
id: '/apps/',
path: '/apps/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedAgentIndexRoute = AuthenticatedAgentIndexRouteImport.update({
id: '/agent/',
path: '/agent/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthLoginIndexRoute = AuthLoginIndexRouteImport.update({
id: '/login/',
path: '/login/',
getParentRoute: () => AuthRoute, getParentRoute: () => AuthRoute,
} as any) } as any)
const AuthenticatedRoomRoomNameIndexRoute = const AuthDeviceIndexRoute = AuthDeviceIndexRouteImport.update({
AuthenticatedRoomRoomNameIndexRouteImport.update({ id: '/device/',
id: '/room/$roomName/', path: '/device/',
path: '/room/$roomName/', getParentRoute: () => AuthRoute,
getParentRoute: () => AuthenticatedRoute, } as any)
} as any) const AuthDashboardIndexRoute = AuthDashboardIndexRouteImport.update({
id: '/dashboard/',
path: '/dashboard/',
getParentRoute: () => AuthRoute,
} as any)
const AuthCommandIndexRoute = AuthCommandIndexRouteImport.update({
id: '/command/',
path: '/command/',
getParentRoute: () => AuthRoute,
} as any)
const AuthBlacklistIndexRoute = AuthBlacklistIndexRouteImport.update({
id: '/blacklist/',
path: '/blacklist/',
getParentRoute: () => AuthRoute,
} as any)
const AuthAppsIndexRoute = AuthAppsIndexRouteImport.update({
id: '/apps/',
path: '/apps/',
getParentRoute: () => AuthRoute,
} as any)
const AuthAgentIndexRoute = AuthAgentIndexRouteImport.update({
id: '/agent/',
path: '/agent/',
getParentRoute: () => AuthRoute,
} as any)
const authLoginIndexRoute = authLoginIndexRouteImport.update({
id: '/(auth)/login/',
path: '/login/',
getParentRoute: () => rootRouteImport,
} as any)
const AuthRoomRoomNameIndexRoute = AuthRoomRoomNameIndexRouteImport.update({
id: '/room/$roomName/',
path: '/room/$roomName/',
getParentRoute: () => AuthRoute,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof AuthLoginIndexRoute '/login': typeof authLoginIndexRoute
'/agent': typeof AuthenticatedAgentIndexRoute '/agent': typeof AuthAgentIndexRoute
'/apps': typeof AuthenticatedAppsIndexRoute '/apps': typeof AuthAppsIndexRoute
'/blacklist': typeof AuthenticatedBlacklistIndexRoute '/blacklist': typeof AuthBlacklistIndexRoute
'/command': typeof AuthenticatedCommandIndexRoute '/command': typeof AuthCommandIndexRoute
'/device': typeof AuthenticatedDeviceIndexRoute '/dashboard': typeof AuthDashboardIndexRoute
'/room': typeof AuthenticatedRoomIndexRoute '/device': typeof AuthDeviceIndexRoute
'/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute '/room': typeof AuthRoomIndexRoute
'/room/$roomName': typeof AuthRoomRoomNameIndexRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof AuthLoginIndexRoute '/login': typeof authLoginIndexRoute
'/agent': typeof AuthenticatedAgentIndexRoute '/agent': typeof AuthAgentIndexRoute
'/apps': typeof AuthenticatedAppsIndexRoute '/apps': typeof AuthAppsIndexRoute
'/blacklist': typeof AuthenticatedBlacklistIndexRoute '/blacklist': typeof AuthBlacklistIndexRoute
'/command': typeof AuthenticatedCommandIndexRoute '/command': typeof AuthCommandIndexRoute
'/device': typeof AuthenticatedDeviceIndexRoute '/dashboard': typeof AuthDashboardIndexRoute
'/room': typeof AuthenticatedRoomIndexRoute '/device': typeof AuthDeviceIndexRoute
'/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute '/room': typeof AuthRoomIndexRoute
'/room/$roomName': typeof AuthRoomRoomNameIndexRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/_auth': typeof AuthRouteWithChildren '/_auth': typeof AuthRouteWithChildren
'/_authenticated': typeof AuthenticatedRouteWithChildren '/(auth)/login/': typeof authLoginIndexRoute
'/_auth/login/': typeof AuthLoginIndexRoute '/_auth/agent/': typeof AuthAgentIndexRoute
'/_authenticated/agent/': typeof AuthenticatedAgentIndexRoute '/_auth/apps/': typeof AuthAppsIndexRoute
'/_authenticated/apps/': typeof AuthenticatedAppsIndexRoute '/_auth/blacklist/': typeof AuthBlacklistIndexRoute
'/_authenticated/blacklist/': typeof AuthenticatedBlacklistIndexRoute '/_auth/command/': typeof AuthCommandIndexRoute
'/_authenticated/command/': typeof AuthenticatedCommandIndexRoute '/_auth/dashboard/': typeof AuthDashboardIndexRoute
'/_authenticated/device/': typeof AuthenticatedDeviceIndexRoute '/_auth/device/': typeof AuthDeviceIndexRoute
'/_authenticated/room/': typeof AuthenticatedRoomIndexRoute '/_auth/room/': typeof AuthRoomIndexRoute
'/_authenticated/room/$roomName/': typeof AuthenticatedRoomRoomNameIndexRoute '/_auth/room/$roomName/': typeof AuthRoomRoomNameIndexRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
@ -124,6 +123,7 @@ export interface FileRouteTypes {
| '/apps' | '/apps'
| '/blacklist' | '/blacklist'
| '/command' | '/command'
| '/dashboard'
| '/device' | '/device'
| '/room' | '/room'
| '/room/$roomName' | '/room/$roomName'
@ -135,6 +135,7 @@ export interface FileRouteTypes {
| '/apps' | '/apps'
| '/blacklist' | '/blacklist'
| '/command' | '/command'
| '/dashboard'
| '/device' | '/device'
| '/room' | '/room'
| '/room/$roomName' | '/room/$roomName'
@ -142,32 +143,25 @@ export interface FileRouteTypes {
| '__root__' | '__root__'
| '/' | '/'
| '/_auth' | '/_auth'
| '/_authenticated' | '/(auth)/login/'
| '/_auth/login/' | '/_auth/agent/'
| '/_authenticated/agent/' | '/_auth/apps/'
| '/_authenticated/apps/' | '/_auth/blacklist/'
| '/_authenticated/blacklist/' | '/_auth/command/'
| '/_authenticated/command/' | '/_auth/dashboard/'
| '/_authenticated/device/' | '/_auth/device/'
| '/_authenticated/room/' | '/_auth/room/'
| '/_authenticated/room/$roomName/' | '/_auth/room/$roomName/'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AuthRoute: typeof AuthRouteWithChildren AuthRoute: typeof AuthRouteWithChildren
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren authLoginIndexRoute: typeof authLoginIndexRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/_authenticated': {
id: '/_authenticated'
path: ''
fullPath: ''
preLoaderRoute: typeof AuthenticatedRouteImport
parentRoute: typeof rootRouteImport
}
'/_auth': { '/_auth': {
id: '/_auth' id: '/_auth'
path: '' path: ''
@ -182,103 +176,100 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/_authenticated/room/': { '/_auth/room/': {
id: '/_authenticated/room/' id: '/_auth/room/'
path: '/room' path: '/room'
fullPath: '/room' fullPath: '/room'
preLoaderRoute: typeof AuthenticatedRoomIndexRouteImport preLoaderRoute: typeof AuthRoomIndexRouteImport
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'
fullPath: '/command'
preLoaderRoute: typeof AuthenticatedCommandIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/blacklist/': {
id: '/_authenticated/blacklist/'
path: '/blacklist'
fullPath: '/blacklist'
preLoaderRoute: typeof AuthenticatedBlacklistIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/apps/': {
id: '/_authenticated/apps/'
path: '/apps'
fullPath: '/apps'
preLoaderRoute: typeof AuthenticatedAppsIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/agent/': {
id: '/_authenticated/agent/'
path: '/agent'
fullPath: '/agent'
preLoaderRoute: typeof AuthenticatedAgentIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_auth/login/': {
id: '/_auth/login/'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof AuthLoginIndexRouteImport
parentRoute: typeof AuthRoute parentRoute: typeof AuthRoute
} }
'/_authenticated/room/$roomName/': { '/_auth/device/': {
id: '/_authenticated/room/$roomName/' id: '/_auth/device/'
path: '/device'
fullPath: '/device'
preLoaderRoute: typeof AuthDeviceIndexRouteImport
parentRoute: typeof AuthRoute
}
'/_auth/dashboard/': {
id: '/_auth/dashboard/'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof AuthDashboardIndexRouteImport
parentRoute: typeof AuthRoute
}
'/_auth/command/': {
id: '/_auth/command/'
path: '/command'
fullPath: '/command'
preLoaderRoute: typeof AuthCommandIndexRouteImport
parentRoute: typeof AuthRoute
}
'/_auth/blacklist/': {
id: '/_auth/blacklist/'
path: '/blacklist'
fullPath: '/blacklist'
preLoaderRoute: typeof AuthBlacklistIndexRouteImport
parentRoute: typeof AuthRoute
}
'/_auth/apps/': {
id: '/_auth/apps/'
path: '/apps'
fullPath: '/apps'
preLoaderRoute: typeof AuthAppsIndexRouteImport
parentRoute: typeof AuthRoute
}
'/_auth/agent/': {
id: '/_auth/agent/'
path: '/agent'
fullPath: '/agent'
preLoaderRoute: typeof AuthAgentIndexRouteImport
parentRoute: typeof AuthRoute
}
'/(auth)/login/': {
id: '/(auth)/login/'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof authLoginIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_auth/room/$roomName/': {
id: '/_auth/room/$roomName/'
path: '/room/$roomName' path: '/room/$roomName'
fullPath: '/room/$roomName' fullPath: '/room/$roomName'
preLoaderRoute: typeof AuthenticatedRoomRoomNameIndexRouteImport preLoaderRoute: typeof AuthRoomRoomNameIndexRouteImport
parentRoute: typeof AuthenticatedRoute parentRoute: typeof AuthRoute
} }
} }
} }
interface AuthRouteChildren { interface AuthRouteChildren {
AuthLoginIndexRoute: typeof AuthLoginIndexRoute AuthAgentIndexRoute: typeof AuthAgentIndexRoute
AuthAppsIndexRoute: typeof AuthAppsIndexRoute
AuthBlacklistIndexRoute: typeof AuthBlacklistIndexRoute
AuthCommandIndexRoute: typeof AuthCommandIndexRoute
AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute
AuthDeviceIndexRoute: typeof AuthDeviceIndexRoute
AuthRoomIndexRoute: typeof AuthRoomIndexRoute
AuthRoomRoomNameIndexRoute: typeof AuthRoomRoomNameIndexRoute
} }
const AuthRouteChildren: AuthRouteChildren = { const AuthRouteChildren: AuthRouteChildren = {
AuthLoginIndexRoute: AuthLoginIndexRoute, AuthAgentIndexRoute: AuthAgentIndexRoute,
AuthAppsIndexRoute: AuthAppsIndexRoute,
AuthBlacklistIndexRoute: AuthBlacklistIndexRoute,
AuthCommandIndexRoute: AuthCommandIndexRoute,
AuthDashboardIndexRoute: AuthDashboardIndexRoute,
AuthDeviceIndexRoute: AuthDeviceIndexRoute,
AuthRoomIndexRoute: AuthRoomIndexRoute,
AuthRoomRoomNameIndexRoute: AuthRoomRoomNameIndexRoute,
} }
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren) const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
interface AuthenticatedRouteChildren {
AuthenticatedAgentIndexRoute: typeof AuthenticatedAgentIndexRoute
AuthenticatedAppsIndexRoute: typeof AuthenticatedAppsIndexRoute
AuthenticatedBlacklistIndexRoute: typeof AuthenticatedBlacklistIndexRoute
AuthenticatedCommandIndexRoute: typeof AuthenticatedCommandIndexRoute
AuthenticatedDeviceIndexRoute: typeof AuthenticatedDeviceIndexRoute
AuthenticatedRoomIndexRoute: typeof AuthenticatedRoomIndexRoute
AuthenticatedRoomRoomNameIndexRoute: typeof AuthenticatedRoomRoomNameIndexRoute
}
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
AuthenticatedAgentIndexRoute: AuthenticatedAgentIndexRoute,
AuthenticatedAppsIndexRoute: AuthenticatedAppsIndexRoute,
AuthenticatedBlacklistIndexRoute: AuthenticatedBlacklistIndexRoute,
AuthenticatedCommandIndexRoute: AuthenticatedCommandIndexRoute,
AuthenticatedDeviceIndexRoute: AuthenticatedDeviceIndexRoute,
AuthenticatedRoomIndexRoute: AuthenticatedRoomIndexRoute,
AuthenticatedRoomRoomNameIndexRoute: AuthenticatedRoomRoomNameIndexRoute,
}
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
AuthenticatedRouteChildren,
)
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AuthRoute: AuthRouteWithChildren, AuthRoute: AuthRouteWithChildren,
AuthenticatedRoute: AuthenticatedRouteWithChildren, authLoginIndexRoute: authLoginIndexRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)

View File

@ -1,4 +1,4 @@
import { createFileRoute, redirect } from '@tanstack/react-router' import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
import { import {
Card, Card,
CardContent, CardContent,
@ -14,6 +14,8 @@ import {
formOptions, formOptions,
useForm, useForm,
} from '@tanstack/react-form' } from '@tanstack/react-form'
import { useLogin } from '@/hooks/queries'
import { toast } from 'sonner'
interface LoginFormProps { interface LoginFormProps {
username: string username: string
@ -29,26 +31,34 @@ const formOpts = formOptions({
defaultValues: defaultInput, defaultValues: defaultInput,
}) })
export const Route = createFileRoute('/_auth/login/')({ export const Route = createFileRoute('/(auth)/login/')({
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const { authToken } = context.auth const { token } = context.auth
if (authToken) throw redirect({ to: '/' }) if (token) throw redirect({ to: '/' })
}, },
component: LoginForm, component: LoginForm,
}) })
function LoginForm() { function LoginForm() {
const navigate = useNavigate()
const loginMutation = useLogin()
const form = useForm({ const form = useForm({
...formOpts, ...formOpts,
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
console.log('Submitting login form with values:', value) try {
await loginMutation.mutateAsync({
username: value.username,
password: value.password,
})
// Giả lập đăng nhập toast.success('Đăng nhập thành công!')
if (value.username === 'admin' && value.password === '123456') { navigate({ to: '/' })
alert('Đăng nhập thành công!') } catch (error: any) {
// Thêm xử lý lưu token, redirect... console.error('Login error:', error)
} else { toast.error(
alert('Tài khoản hoặc mật khẩu không đúng.') error.response?.data?.message || 'Tài khoản hoặc mật khẩu không đúng.'
)
} }
}, },
}) })
@ -110,8 +120,8 @@ function LoginForm() {
)} )}
</form.Field> </form.Field>
<Button type="submit" className="w-full"> <Button type="submit" className="w-full" disabled={loginMutation.isPending}>
Đăng nhập {loginMutation.isPending ? 'Đang đăng nhập...' : 'Đăng nhập'}
</Button> </Button>
</form> </form>
</CardContent> </CardContent>

View File

@ -1,22 +1,37 @@
import { Outlet, createRootRouteWithContext, HeadContent } from '@tanstack/react-router' import ErrorRoute from "@/components/pages/error-route";
import type { AuthTokenProps } from '@/hooks/useAuthtoken' import NotFound from "@/components/pages/not-found";
import { type IAuthContext } from "@/types/auth";
import { QueryClient } from "@tanstack/react-query";
import {
createRootRouteWithContext,
HeadContent,
Outlet,
} from "@tanstack/react-router";
export interface RouterContext { export interface BreadcrumbItem {
auth: AuthTokenProps title: string;
path: string;
} }
export const Route = createRootRouteWithContext<RouterContext>()({ export interface MyRouterContext {
head: () => ({ auth: IAuthContext;
meta: [ queryClient: QueryClient;
{ title: "Quản lý phòng máy" }, breadcrumbs?: BreadcrumbItem[];
{ name: "description", content: "Ứng dụng quản lý thiết bị và phần mềm" }, }
],
}),
component: () => (
<>
<HeadContent />
<Outlet />
</>
),
})
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => {
return (
<>
<HeadContent />
<Outlet />
</>
);
},
notFoundComponent: () => {
return <NotFound />;
},
errorComponent: ({ error }) => {
return <ErrorRoute error={error.message} />;
},
});

View File

@ -1,16 +1,21 @@
import {createFileRoute, Outlet, redirect} from '@tanstack/react-router' import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import AppLayout from '@/layouts/app-layout'
export const Route = createFileRoute('/_auth')({ export const Route = createFileRoute('/_auth')({
beforeLoad: async ({context}) => {
const {authToken} = context.auth // beforeLoad: async ({context}) => {
if (authToken) { // const {token} = context.auth
throw redirect({to: '/'}) // if (token == null) {
} // throw redirect({to: '/login'})
}, // }
component:AuthLayout , // },
component: AuthenticatedLayout,
}) })
function AuthLayout() {
function AuthenticatedLayout() {
return ( return (
<AppLayout>
<Outlet /> <Outlet />
</AppLayout>
) )
} }

View File

@ -1,30 +1,27 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { AppManagerTemplate } from "@/template/app-manager-template"; import { AppManagerTemplate } from "@/template/app-manager-template";
import { useQueryData } from "@/hooks/useQueryData"; import {
import { useMutationData } from "@/hooks/useMutationData"; useGetAgentVersion,
import { API_ENDPOINTS } from "@/config/api"; useGetRoomList,
useUploadSoftware,
useUpdateAgent,
} from "@/hooks/queries";
import { toast } from "sonner"; import { toast } from "sonner";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import type { AxiosProgressEvent } from "axios"; import type { AxiosProgressEvent } from "axios";
import type { Version } from "@/types/file"; import type { Version } from "@/types/file";
export const Route = createFileRoute("/_authenticated/agent/")({ export const Route = createFileRoute("/_auth/agent/")({
head: () => ({ meta: [{ title: "Quản lý Agent" }] }), head: () => ({ meta: [{ title: "Quản lý Agent" }] }),
component: AgentsPage, component: AgentsPage,
}); });
function AgentsPage() { function AgentsPage() {
// Lấy danh sách version // Lấy danh sách version
const { data, isLoading } = useQueryData({ const { data, isLoading } = useGetAgentVersion();
queryKey: ["agent-version"],
url: API_ENDPOINTS.APP_VERSION.GET_VERSION,
});
// Lấy danh sách phòng // Lấy danh sách phòng
const { data: roomData } = useQueryData({ const { data: roomData } = useGetRoomList();
queryKey: ["rooms"],
url: API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST,
});
const versionList: Version[] = Array.isArray(data) const versionList: Version[] = Array.isArray(data)
? data ? data
@ -32,44 +29,32 @@ function AgentsPage() {
? [data] ? [data]
: []; : [];
const uploadMutation = useMutationData<FormData>({ const uploadMutation = useUploadSoftware();
url: API_ENDPOINTS.APP_VERSION.UPLOAD,
method: "POST",
invalidate: [["agent-version"]],
onSuccess: () => toast.success("Upload thành công!"),
onError: (error) => {
console.error("Upload error:", error);
toast.error("Upload thất bại!");
},
});
const updateMutation = useMutationData<void>({ const updateMutation = useUpdateAgent();
url: "",
method: "POST",
onSuccess: () => toast.success("Đã gửi yêu cầu update!"),
onError: (error) => {
console.error("Update mutation error:", error);
toast.error("Gửi yêu cầu thất bại!");
},
});
const handleUpload = async ( const handleUpload = async (
fd: FormData, fd: FormData,
config?: { onUploadProgress?: (e: AxiosProgressEvent) => void } config?: { onUploadProgress?: (e: AxiosProgressEvent) => void }
) => { ) => {
return uploadMutation.mutateAsync({ try {
data: fd, await uploadMutation.mutateAsync({
config, formData: fd,
}); onUploadProgress: config?.onUploadProgress,
});
toast.success("Upload thành công!");
} catch (error: any) {
console.error("Upload error:", error);
toast.error("Upload thất bại!");
}
}; };
const handleUpdate = async (roomNames: string[]) => { const handleUpdate = async (roomNames: string[]) => {
try { try {
for (const roomName of roomNames) { for (const roomName of roomNames) {
await updateMutation.mutateAsync({ await updateMutation.mutateAsync({
url: API_ENDPOINTS.DEVICE_COMM.UPDATE_AGENT(roomName), roomName,
method: "POST", data: {}
data: undefined
}); });
} }
toast.success("Đã gửi yêu cầu update cho các phòng đã chọn!"); toast.success("Đã gửi yêu cầu update cho các phòng đã chọn!");

View File

@ -1,30 +1,30 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { AppManagerTemplate } from "@/template/app-manager-template"; import { AppManagerTemplate } from "@/template/app-manager-template";
import { useQueryData } from "@/hooks/useQueryData"; import {
import { useMutationData } from "@/hooks/useMutationData"; useGetSoftwareList,
import { API_ENDPOINTS } from "@/config/api"; useGetRoomList,
useUploadSoftware,
useDeleteFile,
useAddRequiredFile,
useDeleteRequiredFile,
useInstallMsi,
useDownloadFiles,
} from "@/hooks/queries";
import { toast } from "sonner"; import { toast } from "sonner";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import { useState } from "react";
import type { AxiosProgressEvent } from "axios"; import type { AxiosProgressEvent } from "axios";
import type { Version } from "@/types/file"; import type { Version } from "@/types/file";
import { Check, X } from "lucide-react"; import { Check, X } from "lucide-react";
import { useState } from "react";
export const Route = createFileRoute("/_authenticated/apps/")({ export const Route = createFileRoute("/_auth/apps/")({
head: () => ({ meta: [{ title: "Quản lý phần mềm" }] }), head: () => ({ meta: [{ title: "Quản lý phần mềm" }] }),
component: AppsComponent, component: AppsComponent,
}); });
function AppsComponent() { function AppsComponent() {
const { data, isLoading } = useQueryData({ const { data, isLoading } = useGetSoftwareList();
queryKey: ["software-version"], const { data: roomData } = useGetRoomList();
url: API_ENDPOINTS.APP_VERSION.GET_SOFTWARE,
});
const { data: roomData } = useQueryData({
queryKey: ["rooms"],
url: API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST,
});
const versionList: Version[] = Array.isArray(data) const versionList: Version[] = Array.isArray(data)
? data ? data
@ -34,72 +34,17 @@ function AppsComponent() {
const [table, setTable] = useState<any>(); const [table, setTable] = useState<any>();
const uploadMutation = useMutationData<FormData>({ const uploadMutation = useUploadSoftware();
url: API_ENDPOINTS.APP_VERSION.UPLOAD,
method: "POST",
invalidate: [["software-version"]],
onSuccess: () => toast.success("Upload thành công!"),
onError: (error) => {
console.error("Upload error:", error);
toast.error("Upload thất bại!");
},
});
const installMutation = useMutationData<{ MsiFileIds: number[] }>({ const installMutation = useInstallMsi();
url: "",
method: "POST",
onSuccess: () => toast.success("Đã gửi yêu cầu cài đặt file!"),
onError: (error) => {
console.error("Install error:", error);
toast.error("Gửi yêu cầu thất bại!");
},
});
const downloadMutation = useMutationData<{ MsiFileIds: number[] }>({ const downloadMutation = useDownloadFiles();
url: "",
method: "POST",
onSuccess: () => toast.success("Đã gửi yêu cầu tải file!"),
onError: (error) => {
console.error("Download error:", error);
toast.error("Gửi yêu cầu thất bại!");
},
});
const deleteMutation = useMutationData<{ MsiFileIds: number[] }>({ const deleteMutation = useDeleteFile();
url: API_ENDPOINTS.APP_VERSION.DELETE_FILES + "",
method: "POST",
invalidate: [["software-version"]],
onSuccess: () => toast.success("Xóa phần mềm thành công!"),
onError: (error) => {
console.error("Delete error:", error);
toast.error("Xóa phần mềm thất bại!");
},
});
const addRequiredFileMutation = useMutationData<{ const addRequiredFileMutation = useAddRequiredFile();
fileName: string;
version: string;
}>({
url: API_ENDPOINTS.APP_VERSION.ADD_REQUIRED_FILE,
method: "POST",
invalidate: [["software-version"]],
onSuccess: () => toast.success("Thêm file vào danh sách thành công!"),
onError: (error) => {
console.error("Add required file error:", error);
toast.error("Thêm file vào danh sách thất bại!");
},
});
const deleteRequiredFileMutation = useMutationData<{ id: number }>({ const deleteRequiredFileMutation = useDeleteRequiredFile();
url: "",
method: "POST",
invalidate: [["software-version"]],
onSuccess: () => toast.success("Xóa file khỏi danh sách thành công!"),
onError: (error) => {
console.error("Delete required file error:", error);
toast.error("Xóa file khỏi danh sách thất bại!");
},
});
// Cột bảng // Cột bảng
const columns: ColumnDef<Version>[] = [ const columns: ColumnDef<Version>[] = [
@ -163,10 +108,16 @@ function AppsComponent() {
fd: FormData, fd: FormData,
config?: { onUploadProgress?: (e: AxiosProgressEvent) => void } config?: { onUploadProgress?: (e: AxiosProgressEvent) => void }
) => { ) => {
return uploadMutation.mutateAsync({ try {
data: fd, await uploadMutation.mutateAsync({
config, formData: fd,
}); onUploadProgress: config?.onUploadProgress,
});
toast.success("Upload thành công!");
} catch (error: any) {
console.error("Upload error:", error);
toast.error("Upload thất bại!");
}
}; };
// Callback khi chọn phòng // Callback khi chọn phòng
@ -187,7 +138,7 @@ function AppsComponent() {
try { try {
for (const roomName of roomNames) { for (const roomName of roomNames) {
await installMutation.mutateAsync({ await installMutation.mutateAsync({
url: API_ENDPOINTS.DEVICE_COMM.INSTALL_MSI(roomName), roomName,
data: { MsiFileIds }, data: { MsiFileIds },
}); });
} }
@ -205,7 +156,7 @@ function AppsComponent() {
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
if (selectedRows.length === 0) { if (selectedRows.length === 0) {
toast.error("Vui lòng chọn ít nhất một file để cài đặt!"); toast.error("Vui lòng chọn ít nhất một file để tải!");
return; return;
} }
@ -214,13 +165,13 @@ function AppsComponent() {
try { try {
for (const roomName of roomNames) { for (const roomName of roomNames) {
await downloadMutation.mutateAsync({ await downloadMutation.mutateAsync({
url: API_ENDPOINTS.DEVICE_COMM.DOWNLOAD_FILES(roomName), roomName,
data: { MsiFileIds }, data: { MsiFileIds },
}); });
} }
toast.success("Đã gửi yêu cầu cài đặt phần mềm cho các phòng đã chọn!"); toast.success("Đã gửi yêu cầu tải file cho các phòng đã chọn!");
} catch (e) { } catch (e) {
toast.error("Có lỗi xảy ra khi cài đặt!"); toast.error("Có lỗi xảy ra khi tải!");
} }
}; };
@ -235,6 +186,16 @@ function AppsComponent() {
toast.error("Vui lòng chọn ít nhất một file để xóa!"); toast.error("Vui lòng chọn ít nhất một file để xóa!");
return; return;
} }
try {
for (const row of selectedRows) {
const { id } = row.original;
await deleteMutation.mutateAsync(id);
}
toast.success("Xóa phần mềm thành công!");
} catch (e) {
toast.error("Xóa phần mềm thất bại!");
}
}; };
const handleDeleteFromRequiredList = async () => { const handleDeleteFromRequiredList = async () => {
@ -245,11 +206,9 @@ function AppsComponent() {
try { try {
for (const row of selectedRows) { for (const row of selectedRows) {
const { id } = row.original; const { id } = row.original;
await deleteRequiredFileMutation.mutateAsync({ await deleteRequiredFileMutation.mutateAsync(id);
data: { id },
url: API_ENDPOINTS.APP_VERSION.DELETE_REQUIRED_FILE(id),
});
} }
toast.success("Xóa file khỏi danh sách thành công!");
if (table) { if (table) {
table.setRowSelection({}); table.setRowSelection({});
} }
@ -267,11 +226,9 @@ function AppsComponent() {
try { try {
for (const row of selectedRows) { for (const row of selectedRows) {
const { id } = row.original; const { id } = row.original;
await deleteMutation.mutateAsync({ await deleteMutation.mutateAsync(id);
data: { MsiFileIds: [id] },
url: API_ENDPOINTS.APP_VERSION.DELETE_FILES(id),
});
} }
toast.success("Xóa phần mềm từ server thành công!");
if (table) { if (table) {
table.setRowSelection({}); table.setRowSelection({});
} }
@ -297,9 +254,11 @@ function AppsComponent() {
for (const row of selectedRows) { for (const row of selectedRows) {
const { fileName, version } = row.original; const { fileName, version } = row.original;
await addRequiredFileMutation.mutateAsync({ await addRequiredFileMutation.mutateAsync({
data: { fileName, version }, fileName,
version,
}); });
} }
toast.success("Thêm file vào danh sách thành công!");
table.setRowSelection({}); table.setRowSelection({});
} catch (e) { } catch (e) {
console.error("Add required file error:", e); console.error("Add required file error:", e);

View File

@ -1,7 +1,10 @@
import { API_ENDPOINTS } from "@/config/api"; import {
import { useMutationData } from "@/hooks/useMutationData"; useGetBlacklist,
import { useDeleteData } from "@/hooks/useDeleteData"; useGetRoomList,
import { useQueryData } from "@/hooks/useQueryData"; useAddBlacklist,
useDeleteBlacklist,
useUpdateDeviceBlacklist,
} from "@/hooks/queries";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import type { Blacklist } from "@/types/black-list"; import type { Blacklist } from "@/types/black-list";
@ -9,7 +12,7 @@ import { BlackListManagerTemplate } from "@/template/table-manager-template";
import { toast } from "sonner"; import { toast } from "sonner";
import { useState } from "react"; import { useState } from "react";
export const Route = createFileRoute("/_authenticated/blacklist/")({ export const Route = createFileRoute("/_auth/blacklist/")({
head: () => ({ meta: [{ title: "Danh sách các ứng dụng bị chặn" }] }), head: () => ({ meta: [{ title: "Danh sách các ứng dụng bị chặn" }] }),
component: BlacklistComponent, component: BlacklistComponent,
}); });
@ -18,16 +21,10 @@ function BlacklistComponent() {
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set()); const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
// Lấy danh sách blacklist // Lấy danh sách blacklist
const { data, isLoading } = useQueryData({ const { data, isLoading } = useGetBlacklist();
queryKey: ["blacklist"],
url: API_ENDPOINTS.APP_VERSION.GET_VERSION,
});
// Lấy danh sách phòng // Lấy danh sách phòng
const { data: roomData } = useQueryData({ const { data: roomData = [] } = useGetRoomList();
queryKey: ["rooms"],
url: API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST,
});
const blacklist: Blacklist[] = Array.isArray(data) const blacklist: Blacklist[] = Array.isArray(data)
? (data as Blacklist[]) ? (data as Blacklist[])
@ -70,7 +67,7 @@ function BlacklistComponent() {
<input <input
type="checkbox" type="checkbox"
onChange={(e) => { onChange={(e) => {
if (e.target.checked) { if (e.target.checked && data) {
const allIds = data.map((item: { id: number }) => item.id); const allIds = data.map((item: { id: number }) => item.id);
setSelectedRows(new Set(allIds)); setSelectedRows(new Set(allIds));
} else { } else {
@ -97,42 +94,21 @@ function BlacklistComponent() {
}, },
]; ];
// API thêm blacklist // API mutations
const addNewBlacklistMutation = useMutationData<void>({ const addNewBlacklistMutation = useAddBlacklist();
url: "", const deleteBlacklistMutation = useDeleteBlacklist();
method: "POST", const updateDeviceMutation = useUpdateDeviceBlacklist();
onSuccess: () => toast.success("Thêm mới thành công!"),
onError: () => toast.error("Thêm mới thất bại!"),
});
// API cập nhật thiết bị
const updateDeviceMutation = useMutationData<void>({
url: "",
method: "POST",
onSuccess: () => toast.success("Cập nhật thành công!"),
onError: () => toast.error("Cập nhật thất bại!"),
});
// API xoá
const deleteBlacklistMutation = useDeleteData<void>({
invalidate: [["blacklist"]],
onSuccess: () => toast.success("Xóa thành công!"),
onError: () => toast.error("Xóa thất bại!"),
});
// Thêm blacklist // Thêm blacklist
const handleAddNewBlacklist = async (blacklist: { const handleAddNewBlacklist = async (blacklistData: {
appName: string; appName: string;
processName: string; processName: string;
}) => { }) => {
try { try {
await addNewBlacklistMutation.mutateAsync({ await addNewBlacklistMutation.mutateAsync(blacklistData);
url: API_ENDPOINTS.APP_VERSION.ADD_BLACKLIST, toast.success("Thêm mới thành công!");
method: "POST", } catch (error: any) {
config: { headers: { "Content-Type": "application/json" } }, console.error("Add blacklist error:", error);
data: undefined,
});
} catch {
toast.error("Thêm mới thất bại!"); toast.error("Thêm mới thất bại!");
} }
}; };
@ -141,27 +117,28 @@ function BlacklistComponent() {
const handleDeleteBlacklist = async () => { const handleDeleteBlacklist = async () => {
try { try {
for (const blacklistId of selectedRows) { for (const blacklistId of selectedRows) {
await deleteBlacklistMutation.mutateAsync({ await deleteBlacklistMutation.mutateAsync(blacklistId);
url:
API_ENDPOINTS.APP_VERSION.DELETE_BLACKLIST(blacklistId),
config: { headers: { "Content-Type": "application/json" } },
});
} }
toast.success("Xóa thành công!");
setSelectedRows(new Set()); setSelectedRows(new Set());
} catch {} } catch (error: any) {
console.error("Delete blacklist error:", error);
toast.error("Xóa thất bại!");
}
}; };
const handleUpdateDevice = async (target: string | string[]) => { const handleUpdateDevice = async (target: string | string[]) => {
const targets = Array.isArray(target) ? target : [target]; const targets = Array.isArray(target) ? target : [target];
try { try {
for (const deviceId of targets) { for (const roomName of targets) {
await updateDeviceMutation.mutateAsync({ await updateDeviceMutation.mutateAsync({
url: API_ENDPOINTS.DEVICE_COMM.UPDATE_BLACKLIST(deviceId), roomName,
data: undefined, data: {},
}); });
toast.success(`Đã gửi cập nhật cho ${deviceId}`); toast.success(`Đã gửi cập nhật cho ${roomName}`);
} }
} catch (e) { } catch (e: any) {
console.error("Update device error:", e);
toast.error("Có lỗi xảy ra khi cập nhật!"); toast.error("Có lỗi xảy ra khi cập nhật!");
} }
}; };
@ -175,6 +152,7 @@ function BlacklistComponent() {
isLoading={isLoading} isLoading={isLoading}
rooms={roomData} rooms={roomData}
onAdd={handleAddNewBlacklist} onAdd={handleAddNewBlacklist}
onDelete={handleDeleteBlacklist}
onUpdate={handleUpdateDevice} onUpdate={handleUpdateDevice}
/> />
); );

View File

@ -2,13 +2,18 @@ import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
import { CommandSubmitTemplate } from "@/template/command-submit-template"; import { CommandSubmitTemplate } from "@/template/command-submit-template";
import { CommandRegistryForm, type CommandRegistryFormData } from "@/components/forms/command-registry-form"; import { CommandRegistryForm, type CommandRegistryFormData } from "@/components/forms/command-registry-form";
import { useQueryData } from "@/hooks/useQueryData"; import {
import { useMutationData } from "@/hooks/useMutationData"; useGetCommandList,
import { API_ENDPOINTS, BASE_URL } from "@/config/api"; useGetRoomList,
import type { ColumnDef } from "@tanstack/react-table"; useAddCommand,
useUpdateCommand,
useDeleteCommand,
useSendCommand,
} from "@/hooks/queries";
import { toast } from "sonner"; import { toast } from "sonner";
import { Check, X, Edit2, Trash2 } from "lucide-react"; import { Check, X, Edit2, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import type { ColumnDef } from "@tanstack/react-table";
import type { ShellCommandData } from "@/components/forms/command-form"; import type { ShellCommandData } from "@/components/forms/command-form";
interface CommandRegistry { interface CommandRegistry {
@ -22,7 +27,7 @@ interface CommandRegistry {
updatedAt?: string; updatedAt?: string;
} }
export const Route = createFileRoute("/_authenticated/command/")({ export const Route = createFileRoute("/_auth/command/")({
head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }), head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }),
component: CommandPage, component: CommandPage,
}); });
@ -33,16 +38,10 @@ function CommandPage() {
const [table, setTable] = useState<any>(); const [table, setTable] = useState<any>();
// Fetch commands // Fetch commands
const { data: commands = [], isLoading } = useQueryData({ const { data: commands = [], isLoading } = useGetCommandList();
queryKey: ["commands"],
url: API_ENDPOINTS.COMMAND.GET_COMMANDS,
});
// Fetch rooms // Fetch rooms
const { data: roomData } = useQueryData({ const { data: roomData = [] } = useGetRoomList();
queryKey: ["rooms"],
url: API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST,
});
const commandList: CommandRegistry[] = Array.isArray(commands) const commandList: CommandRegistry[] = Array.isArray(commands)
? commands.map((cmd: any) => ({ ? commands.map((cmd: any) => ({
@ -50,63 +49,13 @@ function CommandPage() {
qoS: cmd.qoS ?? 0, qoS: cmd.qoS ?? 0,
isRetained: cmd.isRetained ?? false, isRetained: cmd.isRetained ?? false,
})) }))
: commands : [];
? [{
...commands,
qoS: commands.qoS ?? 0,
isRetained: commands.isRetained ?? false,
}]
: [];
// Add command mutation // Mutations
const addCommandMutation = useMutationData<CommandRegistryFormData>({ const addCommandMutation = useAddCommand();
url: API_ENDPOINTS.COMMAND.ADD_COMMAND, const updateCommandMutation = useUpdateCommand();
method: "POST", const deleteCommandMutation = useDeleteCommand();
invalidate: [["commands"]], const sendCommandMutation = useSendCommand();
onSuccess: () => toast.success("Thêm lệnh thành công!"),
onError: (error) => {
console.error("Add command error:", error);
toast.error("Thêm lệnh thất bại!");
},
});
// Update command mutation
const updateCommandMutation = useMutationData<CommandRegistryFormData>({
url: "",
method: "POST",
invalidate: [["commands"]],
onSuccess: () => toast.success("Cập nhật lệnh thành công!"),
onError: (error) => {
console.error("Update command error:", error);
toast.error("Cập nhật lệnh thất bại!");
},
});
// Delete command mutation
const deleteCommandMutation = useMutationData<any>({
url: "",
method: "DELETE",
invalidate: [["commands"]],
onSuccess: () => toast.success("Xóa lệnh thành công!"),
onError: (error) => {
console.error("Delete command error:", error);
toast.error("Xóa lệnh thất bại!");
},
});
// Execute command mutation
const executeCommandMutation = useMutationData<{
commandIds?: number[];
command?: ShellCommandData;
}>({
url: "",
method: "POST",
onSuccess: () => toast.success("Gửi yêu cầu thực thi lệnh thành công!"),
onError: (error) => {
console.error("Execute command error:", error);
toast.error("Gửi yêu cầu thực thi thất bại!");
},
});
// Columns for command table // Columns for command table
const columns: ColumnDef<CommandRegistry>[] = [ const columns: ColumnDef<CommandRegistry>[] = [
@ -214,20 +163,24 @@ function CommandPage() {
// Handle form submit // Handle form submit
const handleFormSubmit = async (data: CommandRegistryFormData) => { const handleFormSubmit = async (data: CommandRegistryFormData) => {
if (selectedCommand) { try {
// Update if (selectedCommand) {
await updateCommandMutation.mutateAsync({ // Update
url: BASE_URL + API_ENDPOINTS.COMMAND.UPDATE_COMMAND(selectedCommand.id), await updateCommandMutation.mutateAsync({
data, commandId: selectedCommand.id,
}); data,
} else { });
// Add } else {
await addCommandMutation.mutateAsync({ // Add
data, await addCommandMutation.mutateAsync(data);
}); }
setIsDialogOpen(false);
setSelectedCommand(null);
toast.success(selectedCommand ? "Cập nhật lệnh thành công!" : "Thêm lệnh thành công!");
} catch (error) {
console.error("Form submission error:", error);
toast.error(selectedCommand ? "Cập nhật lệnh thất bại!" : "Thêm lệnh thất bại!");
} }
setIsDialogOpen(false);
setSelectedCommand(null);
}; };
// Handle delete // Handle delete
@ -235,12 +188,11 @@ function CommandPage() {
if (!confirm("Bạn có chắc muốn xóa lệnh này?")) return; if (!confirm("Bạn có chắc muốn xóa lệnh này?")) return;
try { try {
await deleteCommandMutation.mutateAsync({ await deleteCommandMutation.mutateAsync(commandId);
url: API_ENDPOINTS.COMMAND.DELETE_COMMAND(commandId), toast.success("Xóa lệnh thành công!");
data: null,
});
} catch (error) { } catch (error) {
console.error("Delete error:", error); console.error("Delete error:", error);
toast.error("Xóa lệnh thất bại!");
} }
}; };
@ -269,8 +221,8 @@ function CommandPage() {
console.log("[DEBUG] Sending to:", target, "Data:", apiData); console.log("[DEBUG] Sending to:", target, "Data:", apiData);
await executeCommandMutation.mutateAsync({ await sendCommandMutation.mutateAsync({
url: API_ENDPOINTS.DEVICE_COMM.SEND_COMMAND(target), roomName: target,
data: apiData as any, data: apiData as any,
}); });
} }
@ -299,8 +251,8 @@ function CommandPage() {
console.log("[DEBUG] Sending custom to:", target, "Data:", apiData); console.log("[DEBUG] Sending custom to:", target, "Data:", apiData);
await executeCommandMutation.mutateAsync({ await sendCommandMutation.mutateAsync({
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.SEND_COMMAND(target), roomName: target,
data: apiData as any, data: apiData as any,
}); });
} }
@ -337,7 +289,7 @@ function CommandPage() {
} }
onExecuteSelected={handleExecuteSelected} onExecuteSelected={handleExecuteSelected}
onExecuteCustom={handleExecuteCustom} onExecuteCustom={handleExecuteCustom}
isExecuting={executeCommandMutation.isPending} isExecuting={sendCommandMutation.isPending}
rooms={roomData} rooms={roomData}
/> />
); );

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_auth/dashboard/')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/(auth)/dashboard/"!</div>
}

View File

@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/device/')({ export const Route = createFileRoute('/_auth/device/')({
head: () => ({ meta: [{ title: 'Danh sách tất cả thiết bị' }] }), head: () => ({ meta: [{ title: 'Danh sách tất cả thiết bị' }] }),
component: AllDevicesComponent, component: AllDevicesComponent,
}) })

View File

@ -3,16 +3,15 @@ import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { LayoutGrid, TableIcon, Monitor, FolderCheck, Loader2 } from "lucide-react"; import { LayoutGrid, TableIcon, Monitor, FolderCheck, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useQueryData } from "@/hooks/useQueryData"; import { useGetDeviceFromRoom } from "@/hooks/queries";
import { useDeviceEvents } from "@/hooks/useDeviceEvents"; import { useDeviceEvents } from "@/hooks/useDeviceEvents";
import { useClientFolderStatus } from "@/hooks/useClientFolderStatus"; import { useClientFolderStatus } from "@/hooks/useClientFolderStatus";
import { API_ENDPOINTS } from "@/config/api";
import { DeviceGrid } from "@/components/grids/device-grid"; import { DeviceGrid } from "@/components/grids/device-grid";
import { DeviceTable } from "@/components/tables/device-table"; import { DeviceTable } from "@/components/tables/device-table";
import { useMachineNumber } from "@/hooks/useMachineNumber"; import { useMachineNumber } from "@/hooks/useMachineNumber";
import { toast } from "sonner"; import { toast } from "sonner";
export const Route = createFileRoute("/_authenticated/room/$roomName/")({ export const Route = createFileRoute("/_auth/room/$roomName/")({
head: ({ params }) => ({ head: ({ params }) => ({
meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }], meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }],
}), }),
@ -20,7 +19,7 @@ export const Route = createFileRoute("/_authenticated/room/$roomName/")({
}); });
function RoomDetailPage() { function RoomDetailPage() {
const { roomName } = useParams({ from: "/_authenticated/room/$roomName/" }); const { roomName } = useParams({ from: "/_auth/room/$roomName/" });
const [viewMode, setViewMode] = useState<"grid" | "table">("grid"); const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
const [isCheckingFolder, setIsCheckingFolder] = useState(false); const [isCheckingFolder, setIsCheckingFolder] = useState(false);
@ -30,18 +29,16 @@ function RoomDetailPage() {
// Folder status from SSE // Folder status from SSE
const folderStatuses = useClientFolderStatus(roomName); const folderStatuses = useClientFolderStatus(roomName);
const { data: devices = [] } = useQueryData({ const { data: devices = [] } = useGetDeviceFromRoom(roomName);
queryKey: ["devices", roomName],
url: API_ENDPOINTS.DEVICE_COMM.GET_DEVICE_FROM_ROOM(roomName),
});
const parseMachineNumber = useMachineNumber(); const parseMachineNumber = useMachineNumber();
const handleCheckFolderStatus = async () => { const handleCheckFolderStatus = async () => {
try { try {
setIsCheckingFolder(true); setIsCheckingFolder(true);
// Trigger folder status check via the service
const response = await fetch( const response = await fetch(
API_ENDPOINTS.DEVICE_COMM.REQUEST_GET_CLIENT_FOLDER_STATUS(roomName), `/api/device-comm/request-get-client-folder-status?roomName=${encodeURIComponent(roomName)}`,
{ {
method: "POST", method: "POST",
} }
@ -55,6 +52,7 @@ function RoomDetailPage() {
} catch (error) { } catch (error) {
console.error("Check folder error:", error); console.error("Check folder error:", error);
toast.error("Lỗi khi kiểm tra thư mục!"); toast.error("Lỗi khi kiểm tra thư mục!");
} finally {
setIsCheckingFolder(false); setIsCheckingFolder(false);
} }
}; };

View File

@ -1,5 +1,4 @@
import { API_ENDPOINTS } from "@/config/api"; import { useGetRoomList } from "@/hooks/queries";
import { useQueryData } from "@/hooks/useQueryData";
import { useDeviceEvents } from "@/hooks/useDeviceEvents"; import { useDeviceEvents } from "@/hooks/useDeviceEvents";
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { import {
@ -32,7 +31,7 @@ import React from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
export const Route = createFileRoute("/_authenticated/room/")({ export const Route = createFileRoute("/_auth/room/")({
head: () => ({ head: () => ({
meta: [{ title: "Danh sách phòng" }], meta: [{ title: "Danh sách phòng" }],
}), }),
@ -42,10 +41,7 @@ export const Route = createFileRoute("/_authenticated/room/")({
function RoomComponent() { function RoomComponent() {
const navigate = useNavigate(); const navigate = useNavigate();
const { data: roomData = [], isLoading } = useQueryData({ const { data: roomData = [], isLoading } = useGetRoomList();
queryKey: ["rooms"],
url: API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST,
});
const [sorting, setSorting] = React.useState<SortingState>([]); const [sorting, setSorting] = React.useState<SortingState>([]);

View File

@ -1,21 +0,0 @@
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import AppLayout from '@/layouts/app-layout'
export const Route = createFileRoute('/_authenticated')({
// Kiểm tra auth trước khi render
// beforeLoad: async ({context}) => {
// const {authToken} = context.auth
// if (!authToken) {
// throw redirect({to: '/login'})
// }
// },
component: AuthenticatedLayout,
})
function AuthenticatedLayout() {
return (
<AppLayout>
<Outlet />
</AppLayout>
)
}

View File

@ -0,0 +1,127 @@
import axios, { type AxiosProgressEvent } from "axios";
import { API_ENDPOINTS } from "@/config/api";
import type { Version } from "@/types/file";
/**
* Lấy danh sách phiên bản agent
*/
export async function getAgentVersion(): Promise<Version[]> {
const response = await axios.get<Version[]>(API_ENDPOINTS.APP_VERSION.GET_VERSION);
return response.data;
}
/**
* Lấy danh sách phần mềm
*/
export async function getSoftwareList(): Promise<Version[]> {
const response = await axios.get<Version[]>(API_ENDPOINTS.APP_VERSION.GET_SOFTWARE);
return response.data;
}
/**
* Upload file phần mềm/agent
* @param formData - FormData chứa file
* @param onUploadProgress - Callback theo dõi tiến đ upload
*/
export async function uploadSoftware(
formData: FormData,
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
): Promise<{ message: string }> {
const response = await axios.post(API_ENDPOINTS.APP_VERSION.UPLOAD, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress,
});
return response.data;
}
/**
* Lấy danh sách blacklist
*/
export async function getBlacklist(): Promise<any[]> {
const response = await axios.get<any[]>(API_ENDPOINTS.APP_VERSION.GET_BLACKLIST);
return response.data;
}
/**
* Thêm ng dụng vào blacklist
* @param data - Dữ liệu thêm blacklist
*/
export async function addBlacklist(data: any): Promise<{ message: string }> {
const response = await axios.post(API_ENDPOINTS.APP_VERSION.ADD_BLACKLIST, data);
return response.data;
}
/**
* Xóa ng dụng khỏi blacklist
* @param appId - ID ng dụng
*/
export async function deleteBlacklist(appId: number): Promise<{ message: string }> {
const response = await axios.delete(
API_ENDPOINTS.APP_VERSION.DELETE_BLACKLIST(appId)
);
return response.data;
}
/**
* Cập nhật blacklist
* @param appId - ID ng dụng
* @param data - Dữ liệu cập nhật
*/
export async function updateBlacklist(appId: string, data: any): Promise<{ message: string }> {
const response = await axios.put(
API_ENDPOINTS.APP_VERSION.UPDATE_BLACKLIST(appId),
data
);
return response.data;
}
/**
* Yêu cầu cập nhật blacklist
* @param data - Dữ liệu yêu cầu
*/
export async function requestUpdateBlacklist(data: any): Promise<{ message: string }> {
const response = await axios.post(
API_ENDPOINTS.APP_VERSION.REQUEST_UPDATE_BLACKLIST,
data
);
return response.data;
}
/**
* Lấy danh sách file bắt buộc
*/
export async function getRequiredFiles(): Promise<any[]> {
const response = await axios.get<any[]>(API_ENDPOINTS.APP_VERSION.GET_REQUIRED_FILES);
return response.data;
}
/**
* Thêm file bắt buộc
* @param data - Dữ liệu file
*/
export async function addRequiredFile(data: any): Promise<{ message: string }> {
const response = await axios.post(API_ENDPOINTS.APP_VERSION.ADD_REQUIRED_FILE, data);
return response.data;
}
/**
* Xóa file bắt buộc
* @param fileId - ID file
*/
export async function deleteRequiredFile(fileId: number): Promise<{ message: string }> {
const response = await axios.delete(
API_ENDPOINTS.APP_VERSION.DELETE_REQUIRED_FILE(fileId)
);
return response.data;
}
/**
* Xóa file từ server
* @param fileId - ID file
*/
export async function deleteFile(fileId: number): Promise<{ message: string }> {
const response = await axios.delete(API_ENDPOINTS.APP_VERSION.DELETE_FILES(fileId));
return response.data;
}

View File

@ -0,0 +1,86 @@
import axios from "axios";
import { API_ENDPOINTS } from "@/config/api";
import type { LoginResquest, LoginResponse } from "@/types/auth";
/**
* Đăng nhập
* @param credentials - Thông tin đăng nhập
* @returns Response chứa token, name, username, access, role
*/
export async function login(credentials: LoginResquest): Promise<LoginResponse> {
const response = await axios.post<LoginResponse>(
API_ENDPOINTS.AUTH.LOGIN,
credentials
);
return response.data;
}
/**
* Đăng xuất
*/
export async function logout(): Promise<void> {
await axios.delete(API_ENDPOINTS.AUTH.LOGOUT);
}
/**
* Kiểm tra phiên đăng nhập
* @param token - Access token
* @returns Response kiểm tra phiên
*/
export async function ping(token?: string): Promise<{ message: string; code: number }> {
const response = await axios.get(API_ENDPOINTS.AUTH.PING, {
params: token ? { token } : undefined,
});
return response.data;
}
/**
* Lấy CSRF token
* @returns CSRF token
*/
export async function getCsrfToken(): Promise<{ token: string }> {
const response = await axios.get(API_ENDPOINTS.AUTH.CSRF_TOKEN);
return response.data;
}
/**
* Thay đi mật khẩu của user hiện tại
* @param data - Dữ liệu thay đi mật khẩu {currentPassword, newPassword}
*/
export async function changePassword(data: {
currentPassword: string;
newPassword: string;
}): Promise<{ message: string }> {
const response = await axios.put(API_ENDPOINTS.AUTH.CHANGE_PASSWORD, data);
return response.data;
}
/**
* Admin thay đi mật khẩu của user khác
* @param data - Dữ liệu {username, newPassword}
*/
export async function changePasswordAdmin(data: {
username: string;
newPassword: string;
}): Promise<{ message: string }> {
const response = await axios.put(
API_ENDPOINTS.AUTH.CHANGE_PASSWORD_ADMIN,
data
);
return response.data;
}
/**
* Tạo tài khoản mới
* @param data - Dữ liệu tạo tài khoản
*/
export async function createAccount(data: {
userName: string;
password: string;
name: string;
roleId: number;
accessBuildings?: number[];
}): Promise<{ message: string }> {
const response = await axios.post(API_ENDPOINTS.AUTH.CREATE_ACCOUNT, data);
return response.data;
}

View File

@ -0,0 +1,43 @@
import axios from "axios";
import { API_ENDPOINTS } from "@/config/api";
/**
* Lấy danh sách lệnh
*/
export async function getCommandList(): Promise<any[]> {
const response = await axios.get<any[]>(API_ENDPOINTS.COMMAND.GET_COMMANDS);
return response.data;
}
/**
* Thêm lệnh mới
* @param data - Dữ liệu lệnh
*/
export async function addCommand(data: any): Promise<any> {
const response = await axios.post(API_ENDPOINTS.COMMAND.ADD_COMMAND, data);
return response.data;
}
/**
* Cập nhật lệnh
* @param commandId - ID lệnh
* @param data - Dữ liệu cập nhật
*/
export async function updateCommand(commandId: number, data: any): Promise<any> {
const response = await axios.put(
API_ENDPOINTS.COMMAND.UPDATE_COMMAND(commandId),
data
);
return response.data;
}
/**
* Xóa lệnh
* @param commandId - ID lệnh
*/
export async function deleteCommand(commandId: number): Promise<any> {
const response = await axios.delete(
API_ENDPOINTS.COMMAND.DELETE_COMMAND(commandId)
);
return response.data;
}

View File

@ -0,0 +1,115 @@
import axios from "axios";
import { API_ENDPOINTS } from "@/config/api";
import type { DeviceHealthCheck } from "@/types/device";
/**
* Lấy tất cả thiết bị trong hệ thống
*/
export async function getAllDevices(): Promise<any[]> {
const response = await axios.get<any[]>(API_ENDPOINTS.DEVICE_COMM.GET_ALL_DEVICES);
return response.data;
}
/**
* Lấy danh sách phòng
*/
export async function getRoomList(): Promise<any[]> {
const response = await axios.get<any[]>(API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST);
return response.data;
}
/**
* Lấy danh sách thiết bị trong phòng
* @param roomName - Tên phòng
*/
export async function getDeviceFromRoom(roomName: string): Promise<DeviceHealthCheck[]> {
const response = await axios.get<DeviceHealthCheck[]>(
API_ENDPOINTS.DEVICE_COMM.GET_DEVICE_FROM_ROOM(roomName)
);
return response.data;
}
/**
* Tải file về từ phòng
* @param roomName - Tên phòng
* @param data - Dữ liệu yêu cầu
*/
export async function downloadFiles(roomName: string, data: any): Promise<any> {
const response = await axios.post(
API_ENDPOINTS.DEVICE_COMM.DOWNLOAD_FILES(roomName),
data
);
return response.data;
}
/**
* Cài đt MSI file cho phòng
* @param roomName - Tên phòng
* @param data - Dữ liệu MSI
*/
export async function installMsi(roomName: string, data: any): Promise<any> {
const response = await axios.post(
API_ENDPOINTS.DEVICE_COMM.INSTALL_MSI(roomName),
data
);
return response.data;
}
/**
* Cập nhật agent cho phòng
* @param roomName - Tên phòng
* @param data - Dữ liệu cập nhật
*/
export async function updateAgent(roomName: string, data: any): Promise<any> {
const response = await axios.post(
API_ENDPOINTS.DEVICE_COMM.UPDATE_AGENT(roomName),
data
);
return response.data;
}
/**
* Cập nhật blacklist cho phòng
* @param roomName - Tên phòng
* @param data - Dữ liệu blacklist
*/
export async function updateBlacklist(roomName: string, data: any): Promise<any> {
const response = await axios.post(
API_ENDPOINTS.DEVICE_COMM.UPDATE_BLACKLIST(roomName),
data
);
return response.data;
}
/**
* Gửi lệnh shell cho phòng
* @param roomName - Tên phòng
* @param data - Dữ liệu lệnh
*/
export async function sendCommand(roomName: string, data: any): Promise<any> {
const response = await axios.post(
API_ENDPOINTS.DEVICE_COMM.SEND_COMMAND(roomName),
data
);
return response.data;
}
/**
* Thay đi phòng của thiết bị
* @param data - Dữ liệu chuyển phòng
*/
export async function changeDeviceRoom(data: any): Promise<any> {
const response = await axios.post(API_ENDPOINTS.DEVICE_COMM.CHANGE_DEVICE_ROOM, data);
return response.data;
}
/**
* Lấy trạng thái folder client
* @param roomName - Tên phòng
*/
export async function getClientFolderStatus(roomName: string): Promise<any> {
const response = await axios.get(
API_ENDPOINTS.DEVICE_COMM.REQUEST_GET_CLIENT_FOLDER_STATUS(roomName)
);
return response.data;
}

View File

@ -1,21 +0,0 @@
import axios from "axios";
import { queryClient } from "@/main";
import { API_ENDPOINTS } from "@/config/api";
import type { DeviceHealthCheck } from "@/types/device";
export async function fetchDevicesFromRoom(
roomName: string
): Promise<DeviceHealthCheck[]> {
const data = await queryClient.ensureQueryData({
queryKey: ["devices-from-room", roomName],
queryFn: async () => {
const response = await axios.get<DeviceHealthCheck[]>(
API_ENDPOINTS.DEVICE_COMM.GET_DEVICE_FROM_ROOM(roomName)
);
return response.data ?? [];
},
staleTime: 1000 * 60 * 3,
});
return data;
}

12
src/services/index.ts Normal file
View File

@ -0,0 +1,12 @@
// Auth API Services
export * as authService from "./auth.service";
// App Version API Services
export * as appVersionService from "./app-version.service";
// Device Communication API Services
export * as deviceCommService from "./device-comm.service";
// Command API Services
export * as commandService from "./command.service";

93
src/stores/authStore.ts Normal file
View File

@ -0,0 +1,93 @@
import { create } from "zustand";
import { type IAuthContext } from "@/types/auth";
import { useEffect } from "react";
export interface AuthState
extends Omit<IAuthContext, "setAuthenticated" | "setUser"> {
setAuthenticated: (value: boolean) => void;
setAuth: (
username: string,
token: string,
name: string,
acs: number[],
role: { roleName: string; priority: number }
) => void;
initialize: () => void;
}
const getStoredUser = () => localStorage.getItem("username");
export const useAuthStore = create<AuthState>((set) => ({
isAuthenticated: false,
username: "",
token: "",
name: "",
acs: [],
role: {
roleName: "",
priority: -1,
},
initialize: () => {
const username = getStoredUser();
const token = localStorage.getItem("token") || "";
const name = localStorage.getItem("name") || "";
const acsString = localStorage.getItem("acs");
const acs = acsString ? acsString.split(",").map(Number) : [];
const roleName = localStorage.getItem("role") || "";
const priority = localStorage.getItem("priority") || "-1";
set({
username: username || "",
token,
name,
acs,
role: {
roleName,
priority: parseInt(priority),
},
isAuthenticated: !!username,
});
},
setAuth: (
username: string,
token: string,
name: string,
acs: number[],
role: { roleName: string; priority: number }
) => set({ username, token, name, acs, role }),
setAuthenticated: (value: boolean) => set({ isAuthenticated: value }),
logout: () => {
localStorage.removeItem("token");
localStorage.removeItem("name");
localStorage.removeItem("username");
localStorage.removeItem("acs");
localStorage.removeItem("role");
localStorage.removeItem("priority");
set({
isAuthenticated: false,
username: "",
token: "",
name: "",
acs: [],
role: {
roleName: "",
priority: -1,
},
});
},
}));
// Hook to initialize auth store
export function useAuthStoreInitializer() {
const initialize = useAuthStore((state) => state.initialize);
useEffect(() => {
const cleanup = initialize();
return cleanup;
}, [initialize]);
}

View File

@ -20,7 +20,7 @@ import { DeviceSearchDialog } from "@/components/bars/device-searchbar";
import { UploadVersionForm } from "@/components/forms/upload-file-form"; import { UploadVersionForm } from "@/components/forms/upload-file-form";
import type { Room } from "@/types/room"; import type { Room } from "@/types/room";
import { mapRoomsToSelectItems } from "@/helpers/mapRoomToSelectItems"; import { mapRoomsToSelectItems } from "@/helpers/mapRoomToSelectItems";
import { fetchDevicesFromRoom } from "@/services/device.service"; import { getDeviceFromRoom } from "@/services/device-comm.service";
interface AppManagerTemplateProps<TData> { interface AppManagerTemplateProps<TData> {
title: string; title: string;
@ -241,7 +241,7 @@ export function AppManagerTemplate<TData>({
setTimeout(() => window.location.reload(), 500); setTimeout(() => window.location.reload(), 500);
}} }}
rooms={rooms} rooms={rooms}
fetchDevices={fetchDevicesFromRoom} fetchDevices={getDeviceFromRoom}
onSelect={async (deviceIds) => { onSelect={async (deviceIds) => {
if (!onUpdate) { if (!onUpdate) {
setDialogOpen(false); setDialogOpen(false);
@ -299,7 +299,7 @@ export function AppManagerTemplate<TData>({
setTimeout(() => window.location.reload(), 500); setTimeout(() => window.location.reload(), 500);
}} }}
rooms={rooms} rooms={rooms}
fetchDevices={fetchDevicesFromRoom} fetchDevices={getDeviceFromRoom}
onSelect={async (deviceIds) => { onSelect={async (deviceIds) => {
if (!onDownload) { if (!onDownload) {
setDialogOpen(false); setDialogOpen(false);

View File

@ -24,7 +24,7 @@ import { RequestUpdateMenu } from "@/components/menu/request-update-menu";
import { SelectDialog } from "@/components/dialogs/select-dialog"; import { SelectDialog } from "@/components/dialogs/select-dialog";
import { DeviceSearchDialog } from "@/components/bars/device-searchbar"; import { DeviceSearchDialog } from "@/components/bars/device-searchbar";
import { mapRoomsToSelectItems } from "@/helpers/mapRoomToSelectItems"; import { mapRoomsToSelectItems } from "@/helpers/mapRoomToSelectItems";
import { fetchDevicesFromRoom } from "@/services/device.service"; import { getDeviceFromRoom } from "@/services/device-comm.service";
import type { Room } from "@/types/room"; import type { Room } from "@/types/room";
import { toast } from "sonner"; import { toast } from "sonner";
@ -335,7 +335,7 @@ export function CommandSubmitTemplate<T extends { id: number }>({
setTimeout(() => window.location.reload(), 500); setTimeout(() => window.location.reload(), 500);
}} }}
rooms={rooms} rooms={rooms}
fetchDevices={fetchDevicesFromRoom} fetchDevices={getDeviceFromRoom}
onSelect={async (deviceIds) => { onSelect={async (deviceIds) => {
if (!onExecuteSelected) { if (!onExecuteSelected) {
setDialogOpen2(false); setDialogOpen2(false);
@ -393,7 +393,7 @@ export function CommandSubmitTemplate<T extends { id: number }>({
setTimeout(() => window.location.reload(), 500); setTimeout(() => window.location.reload(), 500);
}} }}
rooms={rooms} rooms={rooms}
fetchDevices={fetchDevicesFromRoom} fetchDevices={getDeviceFromRoom}
onSelect={async (deviceIds) => { onSelect={async (deviceIds) => {
try { try {
await handleExecuteCustom(deviceIds); await handleExecuteCustom(deviceIds);

View File

@ -13,7 +13,7 @@ import { SelectDialog } from "@/components/dialogs/select-dialog";
import { DeviceSearchDialog } from "@/components/bars/device-searchbar"; import { DeviceSearchDialog } from "@/components/bars/device-searchbar";
import type { Room } from "@/types/room"; import type { Room } from "@/types/room";
import { mapRoomsToSelectItems } from "@/helpers/mapRoomToSelectItems"; import { mapRoomsToSelectItems } from "@/helpers/mapRoomToSelectItems";
import { fetchDevicesFromRoom } from "@/services/device.service"; import { getDeviceFromRoom } from "@/services/device-comm.service";
interface FormSubmitTemplateProps { interface FormSubmitTemplateProps {
title: string; title: string;
@ -141,7 +141,7 @@ export function FormSubmitTemplate({
setDialogType(null); setDialogType(null);
}} }}
rooms={rooms} rooms={rooms}
fetchDevices={fetchDevicesFromRoom} fetchDevices={getDeviceFromRoom}
onSelect={async (deviceIds) => { onSelect={async (deviceIds) => {
if (!onSubmit) return; if (!onSubmit) return;
try { try {

View File

@ -18,7 +18,7 @@ import { BlacklistForm } from "@/components/forms/black-list-form";
import type { BlacklistFormData } from "@/types/black-list"; import type { BlacklistFormData } from "@/types/black-list";
import type { Room } from "@/types/room"; import type { Room } from "@/types/room";
import { mapRoomsToSelectItems } from "@/helpers/mapRoomToSelectItems"; import { mapRoomsToSelectItems } from "@/helpers/mapRoomToSelectItems";
import { fetchDevicesFromRoom } from "@/services/device.service"; import { getDeviceFromRoom } from "@/services/device-comm.service";
interface BlackListManagerTemplateProps<TData> { interface BlackListManagerTemplateProps<TData> {
title: string; title: string;
@ -141,7 +141,7 @@ export function BlackListManagerTemplate<TData>({
open={dialogOpen && dialogType === "device"} open={dialogOpen && dialogType === "device"}
onClose={() => setDialogOpen(false)} onClose={() => setDialogOpen(false)}
rooms={rooms} rooms={rooms}
fetchDevices={fetchDevicesFromRoom} // ⬅ thêm vào đây fetchDevices={getDeviceFromRoom} // ⬅ thêm vào đây
onSelect={(deviceIds) => onUpdate && onUpdate(deviceIds)} onSelect={(deviceIds) => onUpdate && onUpdate(deviceIds)}
/> />
)} )}

59
src/types/app-sidebar.ts Normal file
View File

@ -0,0 +1,59 @@
import { AppWindow, Building, CircleX, Home, Terminal } from "lucide-react";
import { PermissionEnum } from "./permission";
enum AppSidebarSectionCode {
DASHBOARD = 1,
DEVICES = 2,
DOOR = 4,
DOOR_LAYOUT = 5,
BUILDING_DASHBOARD = 6,
SETUP_DOOR = 7,
DOOR_STATUS = 8,
DEPARTMENTS = 9,
DEPARTMENT_PATHS = 10,
SCHEDULES = 11,
ACCESS_STATUS = 12,
ACCESS_HISTORY = 13,
CONFIG_MANAGER,
APP_VERSION_MANAGER,
DEVICES_APP_VERSION,
HEALTHCHEAK,
LIST_ROLES,
ACCOUNT_PERMISSION,
LIST_ACCOUNT,
DOOR_WARNING,
COMMAND_HISTORY,
ACCESS_ILLEGAL,
ZONES,
MANTRAP,
ROLES,
DEVICES_SYNC_BIO,
}
export const appSidebarSection = {
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
navMain: [
{ title: "Dashboard", to: "/", icon: Home },
{
title: "Danh sách phòng",
to: "/room",
icon: Building,
},
{
title: "Quản lý Agent",
to: "/agent",
icon: AppWindow,
},
{
title: "Quản lý phần mềm",
to: "/apps",
icon: AppWindow,
},
{ title: "Gửi lệnh CMD", to: "/command", icon: Terminal },
{
title: "Danh sách đen",
to: "/blacklist",
icon: CircleX,
},
],
};

30
src/types/auth.ts Normal file
View File

@ -0,0 +1,30 @@
export interface IAuthContext {
isAuthenticated: boolean;
logout: () => void;
username: string;
token: string;
name: string;
acs: number[];
role: {
roleName: string;
priority: number;
};
}
export type LoginResquest = {
username: string;
password: string;
};
export type LoginResponse = {
token: string | null;
name: string | null;
username: string | null;
access: number[] | null;
message: string | null;
code: number | null;
role: {
roleName: string;
priority: number;
};
};

144
src/types/permission.ts Normal file
View File

@ -0,0 +1,144 @@
export type Permission = {
id: number;
name: string;
code: string;
parentId: number | null;
enum: PermissionEnum;
};
export type PermissionOnRole = {
permisionId: number;
roleId: string;
isChecked: number;
permissionName: string;
permissionCode: string;
permissionEnum: PermissionEnum;
parentId: number | null;
};
export enum PermissionEnum {
//ACCESS_OPERATION
ACCESS_OPERATION = 10,
VIEW_ACCESSES = 11,
VIEW_ACCESS_HISTORY = 12,
//APP_CONFIG_OPERATION
APP_CONFIG_OPERATION = 20,
CREATE_APP_CONFIG = 21,
VIEW_APP_CONFIG = 22,
EDIT_APP_CONFIG = 23,
DEL_APP_CONFIG = 24,
//BIOMETRIC_OPERATION
BIOMETRIC_OPERATION = 30,
VIEW_GUEST = 31,
GET_BIO = 32,
GET_SEND_BIO_STATUS = 33,
//BUILDING_OPERATION
BUILDING_OPERATION = 40,
VIEW_BUILDING = 41,
CREATE_BUILDING = 42,
EDIT_BUILDING = 43,
CREATE_LV = 45,
DEL_BUILDING = 44,
//COMMAND_OPERATION
COMMAND_OPERATION = 50,
VIEW_COMMAND = 51,
//DEPARTMENT_OPERATION
DEPARTMENT_OPERATION = 60,
VIEW_DEP = 61,
CREATE_DEP = 62,
EDIT_DEP = 63,
DEL_DEP = 64,
VIEW_PATH = 65,
//DEVICE_OPERATION
DEVICE_OPERATION = 70,
DEL_DEVICE = 71,
EDIT_DEVICE = 73,
VIEW_DEVICE = 74,
//DOOR_OPERATION
DOOR_OPERATION = 80,
SET_DOOR_POSITION = 85,
RESET_DOOR_POSITION = 86,
VIEW_DOOR = 81,
ADD_DOOR = 82,
EDIT_DOOR = 83,
DEL_DOOR = 84,
ADD_DEVICE_TO_DOOR = 87,
REMOVE_DEVICE_FROM_DOOR = 88,
SEND_COMMAND = 801,
SEND_EMERGENCY = 803,
CONTROL_DOOR = 805,
//LEVEL_OPERATION
LEVEL_OPERATION = 90,
UPLOAD_LAYOUT = 91,
VIEW_LEVEL_IN_BUILDING = 92,
EDIT_LV = 93,
DEL_LV = 94,
//PATH_OPERATION
PATH_OPERATION = 100,
CREATE_PATH = 102,
EDIT_PATH = 103,
DEL_PATH = 104,
//PERMISSION_OPERATION
PERMISSION_OPERATION = 110,
VIEW_ALL_PER = 111,
CRE_PER = 112,
DEL_PER = 114,
VIEW_ACCOUNT_BUILDING = 115,
EDIT_ACCOUNT_BUILDING = 116,
//ZONE_OPERATION
ZONE_OPERATION = 120,
CREATE_ZONE = 122,
EDIT_ZONE = 123,
DEL_ZONE = 124,
VIEW_ZONE = 121,
//SCHEDULE_OPERATION
SCHEDULE_OPERATION = 130,
DEL_SCHEDULE = 134,
CREATE_SCHEDULE = 132,
EDIT_SCHEDULE = 133,
VIEW_ALL_SCHEDULE = 131,
//WARNING_OPERATION
WARNING_OPERATION = 140,
VIEW_WARNING = 141,
//USER_OPERATION
USER_OPERATION = 150,
VIEW_USER_ROLE = 151,
VIEW_USER = 152,
EDIT_USER_ROLE = 153,
CRE_USER = 154,
//ROLE_OPERATION
ROLE_OPERATION = 160,
VIEW_ROLES = 161,
CRE_ROLE = 162,
VIEW_ROLE_PER = 165,
EDIT_ROLE_PER = 163,
DEL_ROLE = 164,
// APP VERSION
APP_VERSION_OPERATION = 170,
VIEW_APP_VERSION = 171,
UPLOAD_APK = 172,
CHANGE_PASSWORD = 2,
//Undefined
UNDEFINED = 9999,
ALLOW_ALL = 0
}