refactor code
This commit is contained in:
parent
890b27b96d
commit
b0380b7c9f
341
HOOKS_USAGE_GUIDE.md
Normal file
341
HOOKS_USAGE_GUIDE.md
Normal 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
174
SERVICES_GUIDE.md
Normal 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
328
SERVICES_VS_HOOKS.md
Normal 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
|
||||||
38
src/App.css
38
src/App.css
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
src/components/pages/error-route.tsx
Normal file
24
src/components/pages/error-route.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/components/pages/not-found.tsx
Normal file
35
src/components/pages/not-found.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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`,
|
||||||
|
|
|
||||||
11
src/hooks/queries/index.ts
Normal file
11
src/hooks/queries/index.ts
Normal 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";
|
||||||
186
src/hooks/queries/useAppVersionQueries.ts
Normal file
186
src/hooks/queries/useAppVersionQueries.ts
Normal 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(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
120
src/hooks/queries/useAuthQueries.ts
Normal file
120
src/hooks/queries/useAuthQueries.ts
Normal 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"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
74
src/hooks/queries/useCommandQueries.ts
Normal file
74
src/hooks/queries/useCommandQueries.ts
Normal 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(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
172
src/hooks/queries/useDeviceCommQueries.ts
Normal file
172
src/hooks/queries/useDeviceCommQueries.ts
Normal 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
106
src/hooks/useAuth.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
||||||
84
src/main.tsx
84
src/main.tsx
|
|
@ -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>
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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} />;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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!");
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
9
src/routes/_auth/dashboard/index.tsx
Normal file
9
src/routes/_auth/dashboard/index.tsx
Normal 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>
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -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>([]);
|
||||||
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
127
src/services/app-version.service.ts
Normal file
127
src/services/app-version.service.ts
Normal 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;
|
||||||
|
}
|
||||||
86
src/services/auth.service.ts
Normal file
86
src/services/auth.service.ts
Normal 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;
|
||||||
|
}
|
||||||
43
src/services/command.service.ts
Normal file
43
src/services/command.service.ts
Normal 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;
|
||||||
|
}
|
||||||
115
src/services/device-comm.service.ts
Normal file
115
src/services/device-comm.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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
12
src/services/index.ts
Normal 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
93
src/stores/authStore.ts
Normal 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]);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
59
src/types/app-sidebar.ts
Normal 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
30
src/types/auth.ts
Normal 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
144
src/types/permission.ts
Normal 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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user