import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from '../../axios'; import { toastError } from '../../lib/toastNotification'; import { buildUploadMissionFormData, getProblemXmlMissingNameMessage, UploadMissionRequest, } from '../../api/missionsUpload'; // ─── Типы ──────────────────────────────────────────── type Status = 'idle' | 'loading' | 'successful' | 'failed'; interface Statement { id: number; language: string; statementTexts: Record; mediaFiles?: { id: number; fileName: string; mediaUrl: string }[]; } export interface Mission { id: number; authorId: number; name: string; difficulty: number; tags: string[]; createdAt: string; updatedAt: string; timeLimit: number; memoryLimit: number; statements?: Statement[]; } interface MissionsState { missions: Mission[]; newMissions: Mission[]; currentMission: Mission | null; hasNextPage: boolean; create: { errors?: Record; }; statuses: { fetchList: Status; fetchById: Status; upload: Status; fetchMy: Status; delete: Status; }; error: string | null; } // ─── Инициализация состояния ────────────────────────── const initialState: MissionsState = { missions: [], newMissions: [], currentMission: null, hasNextPage: false, create: {}, statuses: { fetchList: 'idle', fetchById: 'idle', upload: 'idle', fetchMy: 'idle', delete: 'idle', }, error: null, }; // ─── Async Thunks ───────────────────────────────────── // GET /missions export const fetchMissions = createAsyncThunk( 'missions/fetchMissions', async ( { page = 0, pageSize = 100, tags = [], }: { page?: number; pageSize?: number; tags?: string[] }, { rejectWithValue }, ) => { try { const params: any = { page, pageSize }; if (tags.length) params.tags = tags; const response = await axios.get('/missions', { params, paramsSerializer: { indexes: null, }, }); return response.data; // { missions, hasNextPage } } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); // GET /missions export const fetchNewMissions = createAsyncThunk( 'missions/fetchNewMissions', async ( { page = 0, pageSize = 10, tags = [], }: { page?: number; pageSize?: number; tags?: string[] }, { rejectWithValue }, ) => { try { const params: any = { page, pageSize }; if (tags.length) params.tags = tags; const response = await axios.get('/missions', { params, paramsSerializer: { indexes: null, }, }); return response.data; // { missions, hasNextPage } } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); // GET /missions/{id} export const fetchMissionById = createAsyncThunk( 'missions/fetchMissionById', async (id: number, { rejectWithValue }) => { try { const response = await axios.get(`/missions/${id}`); return response.data; // Mission } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); // ✅ GET /missions/my export const fetchMyMissions = createAsyncThunk( 'missions/fetchMyMissions', async (_, { rejectWithValue }) => { try { const response = await axios.get('/missions/my'); return response.data as Mission[]; // массив миссий пользователя } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); // POST /missions/upload export const uploadMission = createAsyncThunk( 'missions/uploadMission', async ( request: UploadMissionRequest, { rejectWithValue }, ) => { try { const formData = buildUploadMissionFormData(request); const response = await axios.post('/missions/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); return response.data; // Mission } catch (err: any) { const status = err?.response?.status; const responseData = err?.response?.data; if (status === 400) { const msg = getProblemXmlMissingNameMessage(responseData); if (msg) { return rejectWithValue({ errors: { missionFile: [msg], }, }); } } if (responseData?.errors) { return rejectWithValue(responseData); } const fallback = typeof responseData === 'string' ? responseData : 'Не удалось загрузить миссию'; return rejectWithValue({ errors: { general: [fallback], }, }); } }, ); // DELETE /missions/{id} export const deleteMission = createAsyncThunk( 'missions/deleteMission', async (id: number, { rejectWithValue }) => { try { await axios.delete(`/missions/${id}`); return id; // возвращаем id удалённой миссии } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); // ─── Slice ──────────────────────────────────────────── const missionsSlice = createSlice({ name: 'missions', initialState, reducers: { setMissionsStatus: ( state, action: PayloadAction<{ key: keyof MissionsState['statuses']; status: Status; }>, ) => { const { key, status } = action.payload; state.statuses[key] = status; }, }, extraReducers: (builder) => { // ─── FETCH MISSIONS ─── builder.addCase(fetchMissions.pending, (state) => { state.statuses.fetchList = 'loading'; state.error = null; }); builder.addCase( fetchMissions.fulfilled, ( state, action: PayloadAction<{ missions: Mission[]; hasNextPage: boolean; }>, ) => { state.statuses.fetchList = 'successful'; state.missions = action.payload.missions; state.hasNextPage = action.payload.hasNextPage; }, ); builder.addCase( fetchMissions.rejected, (state, action: PayloadAction) => { state.statuses.fetchList = 'failed'; const errors = action.payload.errors as Record< string, string[] >; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }, ); // ─── FETCH NEW MISSIONS ─── builder.addCase(fetchNewMissions.pending, (state) => { state.statuses.fetchList = 'loading'; state.error = null; }); builder.addCase( fetchNewMissions.fulfilled, ( state, action: PayloadAction<{ missions: Mission[]; hasNextPage: boolean; }>, ) => { state.statuses.fetchList = 'successful'; state.newMissions = action.payload.missions; state.hasNextPage = action.payload.hasNextPage; }, ); builder.addCase( fetchNewMissions.rejected, (state, action: PayloadAction) => { state.statuses.fetchList = 'failed'; const errors = action.payload.errors as Record< string, string[] >; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }, ); // ─── FETCH MISSION BY ID ─── builder.addCase(fetchMissionById.pending, (state) => { state.statuses.fetchById = 'loading'; state.error = null; }); builder.addCase( fetchMissionById.fulfilled, (state, action: PayloadAction) => { state.statuses.fetchById = 'successful'; state.currentMission = action.payload; }, ); builder.addCase( fetchMissionById.rejected, (state, action: PayloadAction) => { state.statuses.fetchById = 'failed'; const errors = action.payload.errors as Record< string, string[] >; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }, ); // ✅ FETCH MY MISSIONS ─── builder.addCase(fetchMyMissions.pending, (state) => { state.statuses.fetchMy = 'loading'; state.error = null; }); builder.addCase( fetchMyMissions.fulfilled, (state, action: PayloadAction) => { state.statuses.fetchMy = 'successful'; state.missions = action.payload; }, ); builder.addCase( fetchMyMissions.rejected, (state, action: PayloadAction) => { state.statuses.fetchMy = 'failed'; const errors = action.payload.errors as Record< string, string[] >; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }, ); // ─── UPLOAD MISSION ─── builder.addCase(uploadMission.pending, (state) => { state.statuses.upload = 'loading'; state.error = null; }); builder.addCase( uploadMission.fulfilled, (state, action: PayloadAction) => { state.statuses.upload = 'successful'; state.missions.unshift(action.payload); }, ); builder.addCase( uploadMission.rejected, (state, action: PayloadAction) => { state.statuses.upload = 'failed'; const errors = action.payload?.errors as | Record | undefined; if (errors) { Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); state.create.errors = errors; } else { toastError('Не удалось загрузить миссию'); } }, ); // ─── DELETE MISSION ─── builder.addCase(deleteMission.pending, (state) => { state.statuses.delete = 'loading'; state.error = null; }); builder.addCase( deleteMission.fulfilled, (state, action: PayloadAction) => { state.statuses.delete = 'successful'; state.missions = state.missions.filter( (m) => m.id !== action.payload, ); if (state.currentMission?.id === action.payload) { state.currentMission = null; } }, ); builder.addCase( deleteMission.rejected, (state, action: PayloadAction) => { state.statuses.delete = 'failed'; const errors = action.payload.errors as Record< string, string[] >; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }, ); }, }); export const { setMissionsStatus } = missionsSlice.actions; export const missionsReducer = missionsSlice.reducer;