first commit
This commit is contained in:
256
lib/features/users/usersSlice.ts
Normal file
256
lib/features/users/usersSlice.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||
// Users Slice for managing users and deleted users
|
||||
import { fetchClient } from "@/lib/api/fetchClient";
|
||||
import { User } from "@/lib/features/auth/authSlice";
|
||||
|
||||
/** API'den gelen kullanıcıyı frontend User tipine çevirir (avatar -> avatar_url, permissions null ise []). */
|
||||
function mapApiUserToUser(apiUser: Record<string, unknown>): User & { deleted_at?: string } {
|
||||
const roles = (apiUser.roles as User["roles"]) || [];
|
||||
return {
|
||||
id: String(apiUser.id),
|
||||
username: String(apiUser.username ?? ""),
|
||||
email: String(apiUser.email ?? ""),
|
||||
roles: roles.map((r) => ({ ...r, permissions: r.permissions ?? [] })),
|
||||
avatar_url: apiUser.avatar ? String(apiUser.avatar) : (apiUser.avatar_url as string | undefined),
|
||||
deleted_at: apiUser.deleted_at ? String(apiUser.deleted_at) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Types
|
||||
export interface CreateUserRequest {
|
||||
username: string;
|
||||
email: string;
|
||||
password?: string;
|
||||
roles?: string[];
|
||||
avatar?: File;
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
id: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
roles?: string[];
|
||||
avatar?: File;
|
||||
}
|
||||
|
||||
interface UsersState {
|
||||
users: User[]; // Active users
|
||||
deletedUsers: (User & { deleted_at?: string })[]; // Soft-deleted users
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: UsersState = {
|
||||
users: [],
|
||||
deletedUsers: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
// Async Thunks
|
||||
export const fetchUsers = createAsyncThunk(
|
||||
"users/fetchAll",
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetchClient("/admin/users") as { users?: unknown[]; data?: unknown[]; pagination?: unknown };
|
||||
const rawList = response.users ?? response.data ?? (Array.isArray(response) ? response : null);
|
||||
const list = Array.isArray(rawList) ? rawList : [];
|
||||
return list.map((u) => mapApiUserToUser(u as Record<string, unknown>));
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchDeletedUsers = createAsyncThunk(
|
||||
"users/fetchDeleted",
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetchClient("/admin/users/deleted") as { users?: unknown[]; data?: unknown[]; pagination?: unknown };
|
||||
const rawList = response.users ?? response.data ?? (Array.isArray(response) ? response : null);
|
||||
const list = Array.isArray(rawList) ? rawList : [];
|
||||
return list.map((u) => mapApiUserToUser(u as Record<string, unknown>));
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const createUser = createAsyncThunk(
|
||||
"users/create",
|
||||
async (userData: CreateUserRequest, { rejectWithValue }) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("user_name", userData.username);
|
||||
formData.append("email", userData.email);
|
||||
if (userData.password) formData.append("password", userData.password);
|
||||
|
||||
if (userData.roles && userData.roles.length > 0) {
|
||||
userData.roles.forEach((r) => formData.append("roles", r));
|
||||
}
|
||||
|
||||
if (userData.avatar) {
|
||||
formData.append("avatar", userData.avatar);
|
||||
}
|
||||
|
||||
const response = await fetchClient("/admin/users", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const raw = (response as { user?: Record<string, unknown> }).user ?? response;
|
||||
return mapApiUserToUser(raw as Record<string, unknown>);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updateUser = createAsyncThunk(
|
||||
"users/update",
|
||||
async (userData: UpdateUserRequest, { rejectWithValue }) => {
|
||||
try {
|
||||
const { id, username, email, password, roles, avatar } = userData;
|
||||
|
||||
const formData = new FormData();
|
||||
if (username !== undefined) formData.append("user_name", username);
|
||||
if (email !== undefined) formData.append("email", email);
|
||||
if (password) formData.append("password", password);
|
||||
if (roles && roles.length > 0) {
|
||||
roles.forEach((r) => formData.append("roles", r));
|
||||
}
|
||||
if (avatar) {
|
||||
formData.append("avatar", avatar);
|
||||
}
|
||||
|
||||
const response = await fetchClient(`/admin/users/${id}`, {
|
||||
method: "PUT",
|
||||
body: formData,
|
||||
});
|
||||
const raw = (response as { user?: Record<string, unknown> }).user ?? response;
|
||||
return mapApiUserToUser(raw as Record<string, unknown>);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteUser = createAsyncThunk(
|
||||
"users/delete",
|
||||
async (payload: string | { id: string; hard: boolean }, { rejectWithValue }) => {
|
||||
try {
|
||||
const id = typeof payload === "string" ? payload : payload.id;
|
||||
const isHard = typeof payload === "object" && payload.hard;
|
||||
|
||||
const endpoint = `/admin/users/${id}${isHard ? "?hard=true" : ""}`;
|
||||
|
||||
await fetchClient(endpoint, {
|
||||
method: "DELETE",
|
||||
});
|
||||
return { id, isHard };
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const restoreUser = createAsyncThunk(
|
||||
"users/restore",
|
||||
async (id: string, { rejectWithValue }) => {
|
||||
try {
|
||||
await fetchClient(`/admin/users/${id}/restore`, {
|
||||
method: "POST",
|
||||
});
|
||||
return id;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const usersSlice = createSlice({
|
||||
name: "users",
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
// Fetch Active
|
||||
builder.addCase(fetchUsers.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchUsers.fulfilled, (state, action: PayloadAction<User[]>) => {
|
||||
state.isLoading = false;
|
||||
state.users = action.payload;
|
||||
});
|
||||
builder.addCase(fetchUsers.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
|
||||
// Fetch Deleted
|
||||
builder.addCase(fetchDeletedUsers.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchDeletedUsers.fulfilled, (state, action: PayloadAction<User[]>) => {
|
||||
state.isLoading = false;
|
||||
state.deletedUsers = action.payload;
|
||||
});
|
||||
|
||||
// Create
|
||||
builder.addCase(createUser.fulfilled, (state, action: PayloadAction<User>) => {
|
||||
state.isLoading = false;
|
||||
if (action.payload && action.payload.id) {
|
||||
state.users.push(action.payload);
|
||||
}
|
||||
});
|
||||
|
||||
// Update
|
||||
builder.addCase(updateUser.fulfilled, (state, action: PayloadAction<User>) => {
|
||||
state.isLoading = false;
|
||||
if (action.payload && action.payload.id) {
|
||||
const index = state.users.findIndex(u => u.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.users[index] = action.payload;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Delete
|
||||
builder.addCase(deleteUser.fulfilled, (state, action: PayloadAction<{ id: string, isHard: boolean }>) => {
|
||||
state.isLoading = false;
|
||||
const { id, isHard } = action.payload;
|
||||
|
||||
// Remove from active users list (always happens)
|
||||
const deletedUser = state.users.find(u => u.id === id);
|
||||
state.users = state.users.filter(u => u.id !== id);
|
||||
|
||||
// If Soft Delete, add to deletedUsers list (mechanically we should re-fetch, but optimistically we can add if we had the full object)
|
||||
if (!isHard && deletedUser) {
|
||||
state.deletedUsers.unshift({ ...deletedUser, deleted_at: new Date().toISOString() });
|
||||
}
|
||||
|
||||
// If Hard Delete, remove from deletedUsers list too (checking if it was there)
|
||||
if (isHard) {
|
||||
state.deletedUsers = state.deletedUsers.filter(u => u.id !== id);
|
||||
}
|
||||
});
|
||||
|
||||
// Restore
|
||||
builder.addCase(restoreUser.fulfilled, (state, action: PayloadAction<string>) => {
|
||||
const id = action.payload;
|
||||
const restoredUser = state.deletedUsers.find(u => u.id === id);
|
||||
|
||||
// Remove from deleted list
|
||||
state.deletedUsers = state.deletedUsers.filter(u => u.id !== id);
|
||||
|
||||
// Add to active list
|
||||
if (restoredUser) {
|
||||
const { deleted_at, ...user } = restoredUser;
|
||||
state.users.push(user);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default usersSlice.reducer;
|
||||
Reference in New Issue
Block a user