import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from '../../axios'; import { toastError } from '../../lib/toastNotification'; // ===================== // Типы // ===================== type Status = 'idle' | 'loading' | 'successful' | 'failed'; export interface GroupMember { userId: number; username: string; role: string; } export interface Group { id: number; name: string; description: string; members: GroupMember[]; contests: any[]; } // ===================== // Состояние // ===================== interface GroupsState { fetchMyGroups: { groups: Group[]; status: Status; error?: string; }; fetchGroupById: { group?: Group; status: Status; error?: string; }; createGroup: { group?: Group; status: Status; error?: string; }; updateGroup: { group?: Group; status: Status; error?: string; }; deleteGroup: { deletedId?: number; status: Status; error?: string; }; addGroupMember: { status: Status; error?: string; }; removeGroupMember: { status: Status; error?: string; }; fetchGroupJoinLink: { joinLink?: { token: string; expiresAt: string }; status: Status; error?: string; }; joinGroupByToken: { group?: Group; status: Status; error?: string; }; } const initialState: GroupsState = { fetchMyGroups: { groups: [], status: 'idle', error: undefined, }, fetchGroupById: { group: undefined, status: 'idle', error: undefined, }, createGroup: { group: undefined, status: 'idle', error: undefined, }, updateGroup: { group: undefined, status: 'idle', error: undefined, }, deleteGroup: { deletedId: undefined, status: 'idle', error: undefined, }, addGroupMember: { status: 'idle', error: undefined, }, removeGroupMember: { status: 'idle', error: undefined, }, fetchGroupJoinLink: { joinLink: undefined, status: 'idle', error: undefined, }, joinGroupByToken: { group: undefined, status: 'idle', error: undefined, }, }; // ===================== // Async Thunks // ===================== export const createGroup = createAsyncThunk( 'groups/createGroup', async ( { name, description }: { name: string; description: string }, { rejectWithValue }, ) => { try { const response = await axios.post('/groups', { name, description }); return response.data as Group; } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); export const updateGroup = createAsyncThunk( 'groups/updateGroup', async ( { groupId, name, description, }: { groupId: number; name: string; description: string }, { rejectWithValue }, ) => { try { const response = await axios.put(`/groups/${groupId}`, { name, description, }); return response.data as Group; } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); export const deleteGroup = createAsyncThunk( 'groups/deleteGroup', async (groupId: number, { rejectWithValue }) => { try { await axios.delete(`/groups/${groupId}`); return groupId; } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); export const fetchMyGroups = createAsyncThunk( 'groups/fetchMyGroups', async (_, { rejectWithValue }) => { try { const response = await axios.get('/groups/my'); return response.data.groups as Group[]; } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); export const fetchGroupById = createAsyncThunk( 'groups/fetchGroupById', async (groupId: number, { rejectWithValue }) => { try { const response = await axios.get(`/groups/${groupId}`); return response.data as Group; } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); export const addGroupMember = createAsyncThunk( 'groups/addGroupMember', async ( { groupId, userId, role, }: { groupId: number; userId: number; role: string }, { rejectWithValue }, ) => { try { const response = await axios.post(`/groups/${groupId}/members`, { userId, role, }); return response.data; } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); export const removeGroupMember = createAsyncThunk( 'groups/removeGroupMember', async ( { groupId, memberId }: { groupId: number; memberId: number }, { rejectWithValue }, ) => { try { await axios.delete(`/groups/${groupId}/members/${memberId}`); return { groupId, memberId }; } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); // ===================== // Новые Async Thunks // ===================== // Получение актуальной ссылки для присоединения к группе export const fetchGroupJoinLink = createAsyncThunk( 'groups/fetchGroupJoinLink', async (groupId: number, { rejectWithValue }) => { try { const response = await axios.get(`/groups/${groupId}/join-link`); return response.data as { token: string; expiresAt: string }; } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); // Присоединение к группе по токену приглашения export const joinGroupByToken = createAsyncThunk( 'groups/joinGroupByToken', async (token: string, { rejectWithValue }) => { try { const response = await axios.post(`/groups/join/${token}`); return response.data as Group; } catch (err: any) { return rejectWithValue(err.response?.data); } }, ); // ===================== // Slice // ===================== const groupsSlice = createSlice({ name: 'groups', initialState, reducers: { setGroupsStatus: ( state, action: PayloadAction<{ key: keyof GroupsState; status: Status }>, ) => { const { key, status } = action.payload; if (state[key]) { (state[key] as any).status = status; } }, }, extraReducers: (builder) => { // fetchMyGroups builder.addCase(fetchMyGroups.pending, (state) => { state.fetchMyGroups.status = 'loading'; }); builder.addCase( fetchMyGroups.fulfilled, (state, action: PayloadAction) => { state.fetchMyGroups.status = 'successful'; state.fetchMyGroups.groups = action.payload; }, ); builder.addCase(fetchMyGroups.rejected, (state, action: any) => { state.fetchMyGroups.status = 'failed'; const errors = action.payload.errors as Record; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }); // fetchGroupById builder.addCase(fetchGroupById.pending, (state) => { state.fetchGroupById.status = 'loading'; }); builder.addCase( fetchGroupById.fulfilled, (state, action: PayloadAction) => { state.fetchGroupById.status = 'successful'; state.fetchGroupById.group = action.payload; }, ); builder.addCase(fetchGroupById.rejected, (state, action: any) => { state.fetchGroupById.status = 'failed'; const errors = action.payload.errors as Record; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }); // createGroup builder.addCase(createGroup.pending, (state) => { state.createGroup.status = 'loading'; }); builder.addCase( createGroup.fulfilled, (state, action: PayloadAction) => { state.createGroup.status = 'successful'; state.createGroup.group = action.payload; state.fetchMyGroups.groups.push(action.payload); }, ); builder.addCase(createGroup.rejected, (state, action: any) => { state.createGroup.status = 'failed'; const errors = action.payload.errors as Record; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }); // updateGroup builder.addCase(updateGroup.pending, (state) => { state.updateGroup.status = 'loading'; }); builder.addCase( updateGroup.fulfilled, (state, action: PayloadAction) => { state.updateGroup.status = 'successful'; state.updateGroup.group = action.payload; const index = state.fetchMyGroups.groups.findIndex( (g) => g.id === action.payload.id, ); if (index !== -1) state.fetchMyGroups.groups[index] = action.payload; if (state.fetchGroupById.group?.id === action.payload.id) state.fetchGroupById.group = action.payload; }, ); builder.addCase(updateGroup.rejected, (state, action: any) => { state.updateGroup.status = 'failed'; const errors = action.payload.errors as Record; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }); // deleteGroup builder.addCase(deleteGroup.pending, (state) => { state.deleteGroup.status = 'loading'; }); builder.addCase( deleteGroup.fulfilled, (state, action: PayloadAction) => { state.deleteGroup.status = 'successful'; state.deleteGroup.deletedId = action.payload; state.fetchMyGroups.groups = state.fetchMyGroups.groups.filter( (g) => g.id !== action.payload, ); if (state.fetchGroupById.group?.id === action.payload) state.fetchGroupById.group = undefined; }, ); builder.addCase(deleteGroup.rejected, (state, action: any) => { state.deleteGroup.status = 'failed'; const errors = action.payload.errors as Record; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }); // addGroupMember builder.addCase(addGroupMember.pending, (state) => { state.addGroupMember.status = 'loading'; }); builder.addCase(addGroupMember.fulfilled, (state) => { state.addGroupMember.status = 'successful'; }); builder.addCase(addGroupMember.rejected, (state, action: any) => { state.addGroupMember.status = 'failed'; const errors = action.payload.errors as Record; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }); // removeGroupMember builder.addCase(removeGroupMember.pending, (state) => { state.removeGroupMember.status = 'loading'; }); builder.addCase( removeGroupMember.fulfilled, ( state, action: PayloadAction<{ groupId: number; memberId: number }>, ) => { state.removeGroupMember.status = 'successful'; if ( state.fetchGroupById.group && state.fetchGroupById.group.id === action.payload.groupId ) { state.fetchGroupById.group.members = state.fetchGroupById.group.members.filter( (m) => m.userId !== action.payload.memberId, ); } }, ); builder.addCase(removeGroupMember.rejected, (state, action: any) => { state.removeGroupMember.status = 'failed'; const errors = action.payload.errors as Record; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }); // fetchGroupJoinLink builder.addCase(fetchGroupJoinLink.pending, (state) => { state.fetchGroupJoinLink.status = 'loading'; }); builder.addCase( fetchGroupJoinLink.fulfilled, ( state, action: PayloadAction<{ token: string; expiresAt: string }>, ) => { state.fetchGroupJoinLink.status = 'successful'; state.fetchGroupJoinLink.joinLink = action.payload; }, ); builder.addCase(fetchGroupJoinLink.rejected, (state, action: any) => { state.fetchGroupJoinLink.status = 'failed'; const errors = action.payload.errors as Record; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }); // joinGroupByToken builder.addCase(joinGroupByToken.pending, (state) => { state.joinGroupByToken.status = 'loading'; }); builder.addCase( joinGroupByToken.fulfilled, (state, action: PayloadAction) => { state.joinGroupByToken.status = 'successful'; state.joinGroupByToken.group = action.payload; state.fetchMyGroups.groups.push(action.payload); // добавим новую группу в список }, ); builder.addCase(joinGroupByToken.rejected, (state, action: any) => { state.joinGroupByToken.status = 'failed'; const errors = action.payload.errors as Record; Object.values(errors).forEach((messages) => { messages.forEach((msg) => { toastError(msg); }); }); }); }, }); export const { setGroupsStatus } = groupsSlice.actions; export const groupsReducer = groupsSlice.reducer;