Added delete user - Admin Section

This commit is contained in:
Mitchell Magro 2025-11-10 10:43:57 +01:00
parent 7889c98f8d
commit 6a68d93308
7 changed files with 517 additions and 8 deletions

View File

@ -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 }
);
}
}

View File

@ -32,12 +32,7 @@ const Users: React.FC<UsersProps> = ({ users }) => {
<Typography variant="h6">{user.username}</Typography> <Typography variant="h6">{user.username}</Typography>
<Stack direction="row" spacing={1} mt={1}> <Stack direction="row" spacing={1} mt={1}>
<UserRoleCard <UserRoleCard user={user} />
user={user}
isAdmin={true}
lastLogin="small"
merchants={[]} // merchants={Numberuser.allowedMerchantIds}
/>
</Stack> </Stack>
</CardContent> </CardContent>
</Card> </Card>

View 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%;
}
}
}

View 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;

View File

@ -22,6 +22,7 @@ import {
import EditUser from "./EditUser/EditUser"; import EditUser from "./EditUser/EditUser";
import "./User.scss"; import "./User.scss";
import { IUser } from "../Pages/Admin/Users/interfaces"; import { IUser } from "../Pages/Admin/Users/interfaces";
import DeleteUser from "./DeleteUser/DeleteUser";
interface Props { interface Props {
user: IUser; user: IUser;
@ -29,11 +30,16 @@ interface Props {
export default function UserRoleCard({ user }: Props) { export default function UserRoleCard({ user }: Props) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [openDeleteUser, setOpenDeleteUser] = useState(false);
const { username, first_name, last_name, email, groups } = user; const { username, first_name, last_name, email, groups } = user;
const handleEditClick = () => { const handleEditClick = () => {
setIsEditing(!isEditing); setIsEditing(!isEditing);
}; };
const handleDeleteClick = () => {
setOpenDeleteUser(true);
};
return ( return (
<Card sx={{ mb: 2, minWidth: "100%" }}> <Card sx={{ mb: 2, minWidth: "100%" }}>
<CardContent> <CardContent>
@ -69,7 +75,7 @@ export default function UserRoleCard({ user }: Props) {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title="Delete"> <Tooltip title="Delete">
<IconButton> <IconButton onClick={handleDeleteClick}>
<Delete /> <Delete />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@ -98,7 +104,6 @@ export default function UserRoleCard({ user }: Props) {
<Chip key={role} label={role} size="small" /> <Chip key={role} label={role} size="small" />
))} ))}
</Stack> </Stack>
{/* {extraRolesCount && <Chip label={`+${extraRolesCount}`} />} */}
</Stack> </Stack>
</Box> </Box>
<div <div
@ -108,6 +113,13 @@ export default function UserRoleCard({ user }: Props) {
> >
{isEditing && <EditUser user={user} />} {isEditing && <EditUser user={user} />}
</div> </div>
{openDeleteUser && (
<DeleteUser
user={user}
open={openDeleteUser}
onClose={() => setOpenDeleteUser(false)}
/>
)}
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@ -10,6 +10,7 @@ import advancedSearchReducer from "./advanedSearch/advancedSearchSlice";
import authReducer from "./auth/authSlice"; import authReducer from "./auth/authSlice";
import uiReducer from "./ui/uiSlice"; import uiReducer from "./ui/uiSlice";
import metadataReducer from "./metadata/metadataSlice"; import metadataReducer from "./metadata/metadataSlice";
import userReducer from "./user/userSlice";
import userEpics from "./user/epic"; import userEpics from "./user/epic";
import authEpics from "./auth/epic"; import authEpics from "./auth/epic";
import uiEpics from "./ui/epic"; import uiEpics from "./ui/epic";
@ -41,6 +42,7 @@ const rootReducer = combineReducers({
auth: authReducer, auth: authReducer,
metadata: metadataReducer, metadata: metadataReducer,
ui: uiReducer, ui: uiReducer,
user: userReducer,
}); });
const rootEpic = combineEpics(...userEpics, ...authEpics, ...uiEpics); const rootEpic = combineEpics(...userEpics, ...authEpics, ...uiEpics);

200
app/redux/user/userSlice.ts Normal file
View 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;