Files
next-go-blog/lib/features/users/usersSlice.ts
Beyhan Oğur 6d95e27114 first commit
2026-04-26 22:16:43 +03:00

257 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;