Added delete user - Admin Section
This commit is contained in:
parent
7889c98f8d
commit
6a68d93308
@ -43,3 +43,48 @@ export async function PATCH(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const { id } = params;
|
||||
const { cookies } = await import("next/headers");
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get(COOKIE_NAME)?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ message: "Missing Authorization header" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// According to swagger: /api/v1/users/{id}
|
||||
const response = await fetch(`${BE_BASE_URL}/api/v1/users/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Some backends return empty body for DELETE; handle safely
|
||||
let data: any = null;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch {
|
||||
data = { success: response.ok };
|
||||
}
|
||||
|
||||
return NextResponse.json(data ?? { success: response.ok }, {
|
||||
status: response.status,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("Proxy DELETE /api/v1/users/{id} error:", err);
|
||||
return NextResponse.json(
|
||||
{ message: "Internal server error", error: err.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,12 +32,7 @@ const Users: React.FC<UsersProps> = ({ users }) => {
|
||||
<Typography variant="h6">{user.username}</Typography>
|
||||
|
||||
<Stack direction="row" spacing={1} mt={1}>
|
||||
<UserRoleCard
|
||||
user={user}
|
||||
isAdmin={true}
|
||||
lastLogin="small"
|
||||
merchants={[]} // merchants={Numberuser.allowedMerchantIds}
|
||||
/>
|
||||
<UserRoleCard user={user} />
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
126
app/features/UserRoles/DeleteUser/DeleteUser.scss
Normal file
126
app/features/UserRoles/DeleteUser/DeleteUser.scss
Normal file
@ -0,0 +1,126 @@
|
||||
.delete-user__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.delete-user__warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
|
||||
.delete-user__warning-text {
|
||||
margin: 0;
|
||||
color: #856404;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-user__user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f7f7f8;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.delete-user__row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
|
||||
.delete-user__label {
|
||||
font-weight: 600;
|
||||
color: #444;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.delete-user__value {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
color: #222;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-user__error {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #721c24;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.delete-user__actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.delete-user__button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 120px;
|
||||
justify-content: center;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--cancel {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #e8e8e8;
|
||||
border-color: #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
&--delete {
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #c82333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive tweak for smaller screens */
|
||||
@media (max-width: 600px) {
|
||||
.delete-user__content {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.delete-user__actions {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
.delete-user__button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
129
app/features/UserRoles/DeleteUser/DeleteUser.tsx
Normal file
129
app/features/UserRoles/DeleteUser/DeleteUser.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "@/app/components/Modal/Modal";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch } from "@/app/redux/store";
|
||||
import { deleteUser, clearError } from "@/app/redux/user/userSlice";
|
||||
import { IUser } from "../../Pages/Admin/Users/interfaces";
|
||||
import Spinner from "@/app/components/Spinner/Spinner";
|
||||
import { RootState } from "@/app/redux/store";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import "./DeleteUser.scss";
|
||||
|
||||
interface DeleteUserProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
user: IUser | null;
|
||||
}
|
||||
|
||||
const DeleteUser: React.FC<DeleteUserProps> = ({ open, onClose, user }) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const router = useRouter();
|
||||
const { status, error } = useSelector((state: RootState) => state.user);
|
||||
|
||||
const loading = status === "loading";
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!user?.id) {
|
||||
toast.error("No user selected for deletion");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resultAction = await dispatch(deleteUser(user.id));
|
||||
|
||||
if (deleteUser.fulfilled.match(resultAction)) {
|
||||
toast.success(
|
||||
resultAction.payload.message || "User deleted successfully"
|
||||
);
|
||||
dispatch(clearError());
|
||||
router.refresh();
|
||||
onClose();
|
||||
} else if (deleteUser.rejected.match(resultAction)) {
|
||||
toast.error(
|
||||
(resultAction.payload as string) || "Failed to delete user"
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "An unexpected error occurred");
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!loading) {
|
||||
dispatch(clearError());
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={handleClose} title="Delete User">
|
||||
<div className="delete-user__content">
|
||||
<div className="delete-user__warning">
|
||||
<p className="delete-user__warning-text">
|
||||
Are you sure you want to delete this user? This action cannot be
|
||||
undone.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="delete-user__user-info">
|
||||
<div className="delete-user__row">
|
||||
<label className="delete-user__label">Username</label>
|
||||
<div className="delete-user__value">{user.username}</div>
|
||||
</div>
|
||||
<div className="delete-user__row">
|
||||
<label className="delete-user__label">Name</label>
|
||||
<div className="delete-user__value">
|
||||
{user.first_name} {user.last_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="delete-user__row">
|
||||
<label className="delete-user__label">Email</label>
|
||||
<div className="delete-user__value">{user.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="delete-user__error">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="delete-user__actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
disabled={loading}
|
||||
className="delete-user__button delete-user__button--cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={loading}
|
||||
className="delete-user__button delete-user__button--delete"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Spinner size="small" color="#fff" />
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
"Delete User"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteUser;
|
||||
@ -22,6 +22,7 @@ import {
|
||||
import EditUser from "./EditUser/EditUser";
|
||||
import "./User.scss";
|
||||
import { IUser } from "../Pages/Admin/Users/interfaces";
|
||||
import DeleteUser from "./DeleteUser/DeleteUser";
|
||||
|
||||
interface Props {
|
||||
user: IUser;
|
||||
@ -29,11 +30,16 @@ interface Props {
|
||||
|
||||
export default function UserRoleCard({ user }: Props) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [openDeleteUser, setOpenDeleteUser] = useState(false);
|
||||
const { username, first_name, last_name, email, groups } = user;
|
||||
const handleEditClick = () => {
|
||||
setIsEditing(!isEditing);
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
setOpenDeleteUser(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card sx={{ mb: 2, minWidth: "100%" }}>
|
||||
<CardContent>
|
||||
@ -69,7 +75,7 @@ export default function UserRoleCard({ user }: Props) {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete">
|
||||
<IconButton>
|
||||
<IconButton onClick={handleDeleteClick}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@ -98,7 +104,6 @@ export default function UserRoleCard({ user }: Props) {
|
||||
<Chip key={role} label={role} size="small" />
|
||||
))}
|
||||
</Stack>
|
||||
{/* {extraRolesCount && <Chip label={`+${extraRolesCount}`} />} */}
|
||||
</Stack>
|
||||
</Box>
|
||||
<div
|
||||
@ -108,6 +113,13 @@ export default function UserRoleCard({ user }: Props) {
|
||||
>
|
||||
{isEditing && <EditUser user={user} />}
|
||||
</div>
|
||||
{openDeleteUser && (
|
||||
<DeleteUser
|
||||
user={user}
|
||||
open={openDeleteUser}
|
||||
onClose={() => setOpenDeleteUser(false)}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@ -10,6 +10,7 @@ import advancedSearchReducer from "./advanedSearch/advancedSearchSlice";
|
||||
import authReducer from "./auth/authSlice";
|
||||
import uiReducer from "./ui/uiSlice";
|
||||
import metadataReducer from "./metadata/metadataSlice";
|
||||
import userReducer from "./user/userSlice";
|
||||
import userEpics from "./user/epic";
|
||||
import authEpics from "./auth/epic";
|
||||
import uiEpics from "./ui/epic";
|
||||
@ -41,6 +42,7 @@ const rootReducer = combineReducers({
|
||||
auth: authReducer,
|
||||
metadata: metadataReducer,
|
||||
ui: uiReducer,
|
||||
user: userReducer,
|
||||
});
|
||||
|
||||
const rootEpic = combineEpics(...userEpics, ...authEpics, ...uiEpics);
|
||||
|
||||
200
app/redux/user/userSlice.ts
Normal file
200
app/redux/user/userSlice.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { ThunkSuccess, ThunkError, IUserResponse } from "../types";
|
||||
import { IEditUserForm } from "../../features/UserRoles/User.interfaces";
|
||||
import { RootState } from "../store";
|
||||
|
||||
interface UserState {
|
||||
status: "idle" | "loading" | "succeeded" | "failed";
|
||||
error: string | null;
|
||||
message: string;
|
||||
addedUser: IUserResponse | null;
|
||||
editedUser: IUserResponse | null;
|
||||
}
|
||||
|
||||
const initialState: UserState = {
|
||||
status: "idle",
|
||||
error: null,
|
||||
message: "",
|
||||
addedUser: null,
|
||||
editedUser: null,
|
||||
};
|
||||
|
||||
// ---------------- Add User ----------------
|
||||
export const addUser = createAsyncThunk<
|
||||
ThunkSuccess<{ user: IUserResponse; success: boolean }>,
|
||||
IEditUserForm,
|
||||
{ rejectValue: ThunkError; state: RootState }
|
||||
>("user/addUser", async (userData, { rejectWithValue, getState }) => {
|
||||
try {
|
||||
const state = getState();
|
||||
const currentUserId = state.auth.user?.id;
|
||||
console.log("[DEBUG] [ADD-USER] [currentUserId]: ", currentUserId);
|
||||
const res = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ...userData, creator: currentUserId || "" }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
console.log("[DEBUG] [ADD-USER] [data]: ", data);
|
||||
|
||||
if (!res.ok) {
|
||||
return rejectWithValue(data.message || "Failed to create user");
|
||||
}
|
||||
|
||||
return {
|
||||
success: data.success,
|
||||
message: data.message || "User created successfully",
|
||||
user: data.user,
|
||||
} as ThunkSuccess<{ user: IUserResponse; success: boolean }>;
|
||||
} catch (err: unknown) {
|
||||
return rejectWithValue(
|
||||
(err as Error).message || "Network error during user creation"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------- Edit User ----------------
|
||||
export const editUser = createAsyncThunk<
|
||||
ThunkSuccess<{ user: IUserResponse }>,
|
||||
{ id: string; updates: Partial<IUserResponse> },
|
||||
{ rejectValue: ThunkError }
|
||||
>("user/editUser", async ({ id, updates }, { rejectWithValue }) => {
|
||||
try {
|
||||
const res = await fetch(`/api/dashboard/admin/users/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
return rejectWithValue(data.message || "Failed to update user");
|
||||
}
|
||||
|
||||
return {
|
||||
message: data.message || "User updated successfully",
|
||||
user: data.user,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
return rejectWithValue(
|
||||
(err as Error).message || "Network error during user update"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------- Delete User ----------------
|
||||
export const deleteUser = createAsyncThunk<
|
||||
ThunkSuccess<{ id: string }>,
|
||||
string,
|
||||
{ rejectValue: ThunkError }
|
||||
>("user/deleteUser", async (id, { rejectWithValue }) => {
|
||||
try {
|
||||
const res = await fetch(`/api/dashboard/admin/users/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
return rejectWithValue(data.message || "Failed to delete user");
|
||||
}
|
||||
|
||||
return {
|
||||
message: data.message || "User deleted successfully",
|
||||
id,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
return rejectWithValue(
|
||||
(err as Error).message || "Network error during user deletion"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------- Slice ----------------
|
||||
const userSlice = createSlice({
|
||||
name: "user",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearMessage: state => {
|
||||
state.message = "";
|
||||
},
|
||||
clearAddedUser: state => {
|
||||
state.addedUser = null;
|
||||
},
|
||||
clearEditedUser: state => {
|
||||
state.editedUser = null;
|
||||
},
|
||||
clearError: state => {
|
||||
state.error = null;
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder
|
||||
// Add User
|
||||
.addCase(addUser.pending, state => {
|
||||
state.status = "loading";
|
||||
state.message = "Creating user...";
|
||||
state.addedUser = null;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(addUser.fulfilled, (state, action) => {
|
||||
state.status = "succeeded";
|
||||
state.message = action.payload.message;
|
||||
state.addedUser = action.payload.user;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(addUser.rejected, (state, action) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload as string;
|
||||
state.message = action.payload as string;
|
||||
state.addedUser = null;
|
||||
})
|
||||
// Edit User
|
||||
.addCase(editUser.pending, state => {
|
||||
state.status = "loading";
|
||||
state.message = "Updating user...";
|
||||
state.editedUser = null;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(editUser.fulfilled, (state, action) => {
|
||||
state.status = "succeeded";
|
||||
state.message = action.payload.message;
|
||||
state.editedUser = action.payload.user;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(editUser.rejected, (state, action) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload as string;
|
||||
state.message = action.payload as string;
|
||||
state.editedUser = null;
|
||||
})
|
||||
// Delete User
|
||||
.addCase(deleteUser.pending, state => {
|
||||
state.status = "loading";
|
||||
state.message = "Deleting user...";
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(deleteUser.fulfilled, (state, action) => {
|
||||
state.status = "succeeded";
|
||||
state.message = action.payload.message;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(deleteUser.rejected, (state, action) => {
|
||||
state.status = "failed";
|
||||
state.error = action.payload as string;
|
||||
state.message = action.payload as string;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { clearMessage, clearAddedUser, clearEditedUser, clearError } =
|
||||
userSlice.actions;
|
||||
export default userSlice.reducer;
|
||||
Loading…
x
Reference in New Issue
Block a user