diff --git a/front/src/app/store.ts b/front/src/app/store.ts index 7b414ef..4b3ab35 100644 --- a/front/src/app/store.ts +++ b/front/src/app/store.ts @@ -1,10 +1,18 @@ import { configureStore } from "@reduxjs/toolkit"; import authReducer from "../features/auth/authSlice"; +import dashboardReducer from '../features/dashboard/dashboardSlice' export const store = configureStore({ reducer: { auth: authReducer, + dashboard: dashboardReducer, }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: ['persist/PERSIST'], + }, + }), }); export type RootState = ReturnType; diff --git a/front/src/features/dashboard/dashboardSlice.ts b/front/src/features/dashboard/dashboardSlice.ts new file mode 100644 index 0000000..734b01a --- /dev/null +++ b/front/src/features/dashboard/dashboardSlice.ts @@ -0,0 +1,76 @@ +import { createSlice} from '@reduxjs/toolkit'; +import type { DashboardState, Robot, ScanRecord, Statistics, AIPrediction } from '../../model/types/dashboardTypes'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { fetchDashboardData } from './dashboardThunks'; +import { fetchAIPredictions } from './dashboardThunks'; + +const initialState: DashboardState = { + robots: [], + recentScans: [], + statistics: null, + aiPredictions: [], + isWebSocketConnected: false, + isScanAutoUpdate: true, + isLoading: false, +}; + +const dashboardSlice = createSlice({ + name: 'dashboard', + initialState, + reducers: { + setWebSocketConnection: (state, action: PayloadAction) => { + state.isWebSocketConnected = action.payload; + }, + updateRobotData: (state, action: PayloadAction) => { + const index = state.robots.findIndex(r => r.id === action.payload.id); + if (index >= 0) { + state.robots[index] = action.payload; + } else { + state.robots.push(action.payload); + } + }, + addScanRecord: (state, action: PayloadAction) => { + if (state.isScanAutoUpdate) { + state.recentScans.unshift(action.payload); + // Ограничиваем количество записей + if (state.recentScans.length > 20) { + state.recentScans = state.recentScans.slice(0, 20); + } + } + }, + toggleScanAutoUpdate: (state) => { + state.isScanAutoUpdate = !state.isScanAutoUpdate; + }, + updateStatistics: (state, action: PayloadAction) => { + state.statistics = action.payload; + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchDashboardData.pending, (state) => { + state.isLoading = true; + }) + .addCase(fetchDashboardData.fulfilled, (state, action) => { + state.isLoading = false; + state.robots = action.payload.robots; + state.recentScans = action.payload.recent_scans; + state.statistics = action.payload.statistics; + }) + .addCase(fetchDashboardData.rejected, (state) => { + state.isLoading = false; + }) + .addCase(fetchAIPredictions.fulfilled, (state, action) => { + state.aiPredictions = action.payload.predictions; + }); + }, +}); + +export const { + setWebSocketConnection, + updateRobotData, + addScanRecord, + toggleScanAutoUpdate, + updateStatistics, +} = dashboardSlice.actions; + +export default dashboardSlice.reducer; \ No newline at end of file diff --git a/front/src/features/dashboard/dashboardThunks.ts b/front/src/features/dashboard/dashboardThunks.ts new file mode 100644 index 0000000..f36c996 --- /dev/null +++ b/front/src/features/dashboard/dashboardThunks.ts @@ -0,0 +1,51 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; + +// Получение текущего состояния дашборда +export const fetchDashboardData = createAsyncThunk( + 'dashboard/fetchData', + async (_, { rejectWithValue }) => { + try { + const response = await fetch('/api/dashboard/current', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}`, + }, + }); + + if (!response.ok) { + throw new Error('Ошибка загрузки данных'); + } + + return await response.json(); + } catch (error: any) { + return rejectWithValue(error.message); + } + } +); + +// Получение прогнозов от ИИ +export const fetchAIPredictions = createAsyncThunk( + 'dashboard/fetchPredictions', + async (_, { rejectWithValue }) => { + try { + const response = await fetch('/api/ai/predict', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + period_days: 7, + categories: [], + }), + }); + + if (!response.ok) { + throw new Error('Ошибка получения прогнозов'); + } + + return await response.json(); + } catch (error: any) { + return rejectWithValue(error.message); + } + } +); \ No newline at end of file diff --git a/front/src/model/types/dashboardTypes.ts b/front/src/model/types/dashboardTypes.ts new file mode 100644 index 0000000..e09d728 --- /dev/null +++ b/front/src/model/types/dashboardTypes.ts @@ -0,0 +1,46 @@ +export interface Robot { + id: string; + name: string; + batteryLevel: number; + status: 'active' | 'low_battery' | 'offline'; + location: string; + lastUpdate: string; +} + +export interface ScanRecord { + id: string; + timestamp: string; + robotId: string; + zone: string; + productName: string; + productSku: string; + quantity: number; + status: 'ok' | 'low_stock' | 'critical'; +} + +export interface Statistics { + activeRobots: number; + totalRobots: number; + scannedToday: number; + criticalItems: number; + averageBattery: number; +} + +export interface AIPrediction { + id: string; + productName: string; + currentStock: number; + predictedDepletionDate: string; + recommendedOrder: number; + confidence: number; +} + +export interface DashboardState { + robots: Robot[]; + recentScans: ScanRecord[]; + statistics: Statistics | null; + aiPredictions: AIPrediction[]; + isWebSocketConnected: boolean; + isScanAutoUpdate: boolean; + isLoading: boolean; +} \ No newline at end of file