Fixed user edit and introduced user enabled/disabled

This commit is contained in:
Mitchell Magro 2025-11-11 09:19:51 +01:00
parent 568f56bf00
commit a3ad2a8937
14 changed files with 451 additions and 210 deletions

View File

@ -25,10 +25,6 @@ export async function POST(request: Request) {
try {
const payload = decodeJwt(token);
mustChangePassword = payload.MustChangePassword || false;
console.log(
"🔍 Current JWT MustChangePassword flag:",
mustChangePassword
);
} catch (err) {
console.error("❌ Failed to decode current JWT:", err);
}
@ -71,10 +67,6 @@ export async function POST(request: Request) {
const data = await resp.json();
if (!resp.ok) {
console.log("[DEBUG] [CHANGE-PASSWORD] Error response:", {
status: resp.status,
data,
});
return NextResponse.json(
{ success: false, message: data?.message || "Password change failed" },
{ status: resp.status }
@ -89,17 +81,6 @@ export async function POST(request: Request) {
});
if (newToken) {
try {
const payload = decodeJwt(newToken);
console.log("🔍 New JWT payload:", payload);
console.log(
"🔍 must_change_password flag:",
payload.must_change_password
);
} catch (err) {
console.error("❌ Failed to decode new JWT:", err);
}
// Derive maxAge from JWT exp if available; fallback to 12h
let maxAge = 60 * 60 * 12;
try {

View File

@ -1,11 +1,10 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { formatPhoneDisplay } from "@/app/features/UserRoles/utils";
const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:8583";
const COOKIE_NAME = "auth_token";
// Interface matching the backend RegisterRequest
// Interface matching the backend RegisterRequest and frontend IEditUserForm
interface RegisterRequest {
creator: string;
email: string;
@ -18,23 +17,9 @@ interface RegisterRequest {
username: string;
}
// Frontend form interface
interface FrontendRegisterForm {
email: string;
firstName: string;
lastName: string;
username: string;
phone?: string;
jobTitle?: string;
groups?: string[];
merchants?: string[];
creator?: string;
countryCode: string;
}
export async function POST(request: Request) {
try {
const body: FrontendRegisterForm = await request.json();
const body: RegisterRequest = await request.json();
// Get the auth token from cookies
const cookieStore = await cookies();
@ -50,35 +35,6 @@ export async function POST(request: Request) {
);
}
// Validate required fields
const requiredFields = ["email", "firstName", "lastName", "username"];
const missingFields = requiredFields.filter(
field => !body[field as keyof FrontendRegisterForm]
);
if (missingFields.length > 0) {
return NextResponse.json(
{
success: false,
message: `Missing required fields: ${missingFields.join(", ")}`,
},
{ status: 400 }
);
}
// Map frontend payload to backend RegisterRequest format
const registerPayload: RegisterRequest = {
creator: body.creator || "",
email: body.email,
first_name: body.firstName,
groups: body.groups || ["Reader"], // Default to empty array if not provided
job_title: body.jobTitle || "Reader",
last_name: body.lastName,
merchants: body.merchants || ["Win Bot"], // Default to empty array if not provided
phone: body.phone ? formatPhoneDisplay(body.phone, body.countryCode) : "",
username: body.username,
};
// Call backend registration endpoint
const resp = await fetch(`${BE_BASE_URL}/api/v1/auth/register`, {
method: "POST",
@ -86,15 +42,12 @@ export async function POST(request: Request) {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(registerPayload),
body: JSON.stringify(body),
});
console.log("[DEBUG] [REGISTER-PAYLOAD]: ", registerPayload);
// Handle backend response
if (!resp.ok) {
const errorData = await safeJson(resp);
console.log("[DEBUG] [REGISTER-ERROR]: ", errorData);
return NextResponse.json(
{
success: false,
@ -106,8 +59,6 @@ export async function POST(request: Request) {
const data = await resp.json();
console.log("[DEBUG] [REGISTER]: ", data);
return NextResponse.json(
{
success: true,

View File

@ -23,9 +23,6 @@ export async function POST() {
});
const data = await resp.json();
console.log("[DEBUG] [VALIDATE-AUTH][ROUTE][data]: ", data);
return NextResponse.json(data, { status: resp.status });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Unknown error";

View File

@ -4,18 +4,79 @@ import { NextResponse } from "next/server";
const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:5000";
const COOKIE_NAME = "auth_token";
export async function PATCH(
// Field mapping: snake_case input -> { snake_case for data, PascalCase for fields }
// Matches API metadata field_names.users mapping
const FIELD_MAPPING: Record<string, { dataKey: string; fieldName: string }> = {
id: { dataKey: "id", fieldName: "ID" },
email: { dataKey: "email", fieldName: "Email" },
first_name: { dataKey: "first_name", fieldName: "FirstName" },
last_name: { dataKey: "last_name", fieldName: "LastName" },
username: { dataKey: "username", fieldName: "Username" },
phone: { dataKey: "phone", fieldName: "Phone" },
job_title: { dataKey: "job_title", fieldName: "JobTitle" },
password: { dataKey: "password", fieldName: "Password" },
temp_link: { dataKey: "temp_link", fieldName: "TempLink" },
temp_password: { dataKey: "temp_password", fieldName: "TempPassword" },
temp_expiry: { dataKey: "temp_expiry", fieldName: "TempExpiry" },
groups: { dataKey: "groups", fieldName: "Groups" },
merchants: { dataKey: "merchants", fieldName: "Merchants" },
enabled: { dataKey: "enabled", fieldName: "Enabled" },
};
/**
* Transforms frontend snake_case data to backend format
* with data (snake_case) and fields (PascalCase) arrays
*/
function transformUserUpdateData(updates: Record<string, unknown>): {
data: Record<string, unknown>;
fields: string[];
} {
const data: Record<string, unknown> = {};
const fields: string[] = [];
for (const [key, value] of Object.entries(updates)) {
// Skip undefined/null values
if (value === undefined || value === null) {
continue;
}
const mapping = FIELD_MAPPING[key];
if (mapping) {
// Use the dataKey for the data object (snake_case)
data[mapping.dataKey] = value;
// Use the fieldName for the fields array (PascalCase)
fields.push(mapping.fieldName);
} else {
// If no mapping exists, use the key as-is (for backwards compatibility)
data[key] = value;
// Convert snake_case to PascalCase for fields
const pascalCase = key
.split("_")
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join("");
fields.push(pascalCase);
}
}
return { data, fields };
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
try {
console.log("[PATCH /users] - params", params);
const { id } = params;
const { id } = await params;
const body = await request.json();
// Transform the request body to match backend format
const transformedBody = transformUserUpdateData(body);
console.log("[PUT /api/v1/users/{id}] - transformed body", transformedBody);
// Get the auth token from cookies
const { cookies } = await import("next/headers");
const cookieStore = cookies();
const token = (await cookieStore).get(COOKIE_NAME)?.value;
const cookieStore = await cookies();
const token = cookieStore.get(COOKIE_NAME)?.value;
if (!token) {
return NextResponse.json(
@ -24,21 +85,23 @@ export async function PATCH(
);
}
const response = await fetch(`${BE_BASE_URL}/users/${id}`, {
method: "PATCH",
// According to swagger: /api/v1/users/{id}
const response = await fetch(`${BE_BASE_URL}/api/v1/users/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(body),
body: JSON.stringify(transformedBody),
});
const data = await response.json();
return NextResponse.json(data, { status: response.status });
} catch (err: any) {
console.error("Proxy PATCH /users error:", err);
} catch (err: unknown) {
const errorMessage =
err instanceof Error ? err.message : "Unknown error occurred";
return NextResponse.json(
{ message: "Internal server error", error: err.message },
{ message: "Internal server error", error: errorMessage },
{ status: 500 }
);
}
@ -49,7 +112,7 @@ export async function DELETE(
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
const { id } = await params;
const { cookies } = await import("next/headers");
const cookieStore = await cookies();
const token = cookieStore.get(COOKIE_NAME)?.value;
@ -70,7 +133,7 @@ export async function DELETE(
});
// Some backends return empty body for DELETE; handle safely
let data: any = null;
let data: unknown = null;
try {
data = await response.json();
} catch {
@ -80,10 +143,12 @@ export async function DELETE(
return NextResponse.json(data ?? { success: response.ok }, {
status: response.status,
});
} catch (err: any) {
} catch (err: unknown) {
console.error("Proxy DELETE /api/v1/users/{id} error:", err);
const errorMessage =
err instanceof Error ? err.message : "Unknown error occurred";
return NextResponse.json(
{ message: "Internal server error", error: err.message },
{ message: "Internal server error", error: errorMessage },
{ status: 500 }
);
}

View File

@ -28,7 +28,6 @@ export async function GET(request: Request) {
cache: "no-store",
});
console.log("[DEBUG] - Response", response);
const data = await response.json();
return NextResponse.json(data, { status: response.status });
} catch (err: unknown) {

View File

@ -17,6 +17,8 @@ const Users: React.FC<UsersProps> = ({ users }) => {
const [showAddUser, setShowAddUser] = useState(false);
const dispatch = useDispatch<AppDispatch>();
console.log("[Users] - users", users);
return (
<div>
<UserTopBar
@ -30,7 +32,6 @@ const Users: React.FC<UsersProps> = ({ users }) => {
<Card key={user.id} sx={{ mb: 2 }}>
<CardContent>
<Typography variant="h6">{user.username}</Typography>
<Stack direction="row" spacing={1} mt={1}>
<UserRoleCard user={user} />
</Stack>

View File

@ -11,7 +11,7 @@ import {
} from "@mui/material";
import { useSelector, useDispatch } from "react-redux";
import { AppDispatch, RootState } from "@/app/redux/store";
import { updateUserDetails } from "@/app/redux/auth/authSlice";
import { updateUserDetails } from "@/app/redux/user/userSlice";
const SettingsPersonalInfo: React.FC = () => {
const user = useSelector((state: RootState) => state.auth.user);

View File

@ -26,14 +26,13 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose }) => {
);
const [form, setForm] = useState<IEditUserForm>({
username: "",
firstName: "",
lastName: "",
first_name: "",
last_name: "",
email: "",
phone: "",
role: "",
merchants: [],
groups: [],
jobTitle: "",
job_title: "",
});
const [phoneError, setPhoneError] = useState("");
@ -94,12 +93,12 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose }) => {
}
if (
!form.firstName ||
!form.lastName ||
!form.first_name ||
!form.last_name ||
!form.email ||
form.merchants.length === 0 ||
form.groups.length === 0 ||
!form.jobTitle
!form.job_title
) {
return;
}
@ -136,16 +135,16 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose }) => {
required
/>
<input
name="firstName"
name="first_name"
placeholder="First Name"
value={form.firstName}
value={form.first_name}
onChange={handleChange}
required
/>
<input
name="lastName"
name="last_name"
placeholder="Last Name"
value={form.lastName}
value={form.last_name}
onChange={handleChange}
required
/>
@ -235,8 +234,8 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose }) => {
<div className="array-field-container">
<label>Job Title:</label>
<select
name="jobTitle"
value={form.jobTitle}
name="job_title"
value={form.job_title}
onChange={handleChange}
required
className="add-user__select"

View File

@ -4,7 +4,18 @@
flex-wrap: wrap;
gap: 16px;
input {
&__status-toggle {
flex-basis: 100%;
width: 100%;
margin-bottom: 8px;
padding: 12px;
background: #f7f7f8;
border-radius: 6px;
border: 1px solid #e0e0e0;
}
input,
&__select {
flex: 1 1 20%;
min-width: 150px;
box-sizing: border-box;
@ -40,7 +51,62 @@
button:last-child {
color: var(--button-secondary);
border-color: var(--button-secondary);
margin-left: 8;
margin-left: 8px;
}
}
}
.array-field-container {
flex: 1 1 100%;
min-width: 200px;
display: flex;
flex-direction: column;
gap: 8px;
label {
font-weight: 500;
color: #333;
font-size: 0.9rem;
}
.selected-items {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.selected-item {
display: inline-flex;
align-items: center;
gap: 4px;
background: #e3f2fd;
color: #1976d2;
padding: 4px 8px;
border-radius: 16px;
font-size: 0.875rem;
border: 1px solid #bbdefb;
.remove-item {
background: none;
border: none;
color: #1976d2;
cursor: pointer;
font-size: 1.2rem;
line-height: 1;
padding: 0;
margin-left: 4px;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.2s ease;
&:hover {
background-color: rgba(25, 118, 210, 0.1);
}
}
}
}

View File

@ -1,83 +1,169 @@
"use client";
import React from "react";
import { useRouter } from "next/navigation";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "@/app/redux/store";
import { updateUserDetails } from "@/app/redux/user/userSlice";
import { IEditUserForm, EditUserField } from "../User.interfaces";
import { editUser } from "@/services/roles.services";
import { IUser } from "../../Pages/Admin/Users/interfaces";
import toast from "react-hot-toast";
import { Switch, FormControlLabel } from "@mui/material";
import "./EditUser.scss";
import { selectAppMetadata } from "@/app/redux/metadata/selectors";
const EditUser = ({ user }: { user: IUser }) => {
const router = useRouter();
const { username, last_name, email, groups, phone } = user;
const dispatch = useDispatch<AppDispatch>();
const { status } = useSelector((state: RootState) => state.user);
// Get original Metadata from Redux store
const data = useSelector(selectAppMetadata);
const {
merchants: metadataMerchants = [],
groups: metadataGroups = [],
job_titles: metadataJobTitles = [],
} = data || {};
// Get original user data from the API response
const {
username,
first_name,
last_name,
email,
groups,
phone,
enabled,
job_title,
merchants,
} = user;
const [form, setForm] = React.useState<IEditUserForm>({
firstName: username || "",
lastName: last_name || "",
first_name: first_name || "",
last_name: last_name || "",
email: email || "",
role: roles[0] || "",
phone: phone || "",
groups: groups || [],
merchants: merchants || [],
jobTitle: jobTitle || "",
job_title: job_title || "",
username: username || "",
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const name = e.target.name as EditUserField;
const value = e.target.value;
if (name === "phone") {
const filtered = value.replace(/[^0-9+\-\s()]/g, "");
setForm(prev => ({
...prev,
phone: filtered,
}));
setForm(prev => ({ ...prev, phone: filtered }));
return;
}
// Handle array fields (merchants and groups)
if (name === "merchants" || name === "groups") {
if (value === "") {
// If empty selection, set empty array
setForm(prev => ({ ...prev, [name]: [] }));
} else {
// Add the selected value to the array if not already present
setForm(prev => {
const currentArray = prev[name as keyof IEditUserForm] as string[];
if (!currentArray.includes(value)) {
return { ...prev, [name]: [...currentArray, value] };
}
return prev;
});
}
} else {
setForm(prev => ({
...prev,
[name]: value,
}));
// Handle single value fields
setForm(prev => ({ ...prev, [name]: value }));
}
};
const handleResetForm = () => {
setForm({
firstName: "",
lastName: "",
email: "",
role: "",
phone: "",
groups: [],
merchants: [],
jobTitle: "",
username: "",
const handleUpdate = async (
e?: React.FormEvent<HTMLFormElement> | Partial<{ enabled: boolean }>
) => {
if (e && "preventDefault" in e) e.preventDefault();
// Check if this was a toggle-only update
const isToggle = !e || ("enabled" in e && Object.keys(e).length === 1);
const updates: Record<string, unknown> = {};
// Compare form fields vs original user object
Object.entries(form).forEach(([key, value]) => {
const originalValue = (user as unknown as Record<string, unknown>)[key];
// Only include changed values and skip empty ones
if (
value !== "" &&
JSON.stringify(value) !== JSON.stringify(originalValue)
) {
updates[key] = value;
}
});
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Handle enabled toggle separately
if (isToggle) {
updates.enabled = !(enabled ?? true);
}
// Nothing changed — no need to call API
if (Object.keys(updates).length === 0) {
toast("No changes detected");
return;
}
console.log("[handleUpdate] - updates", updates);
try {
await editUser(user.id, form);
router.refresh(); // <- refreshes the page (SSR re-runs)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.log(err.message || "Error creating role");
// setError(err.message || "Error creating role");
const resultAction = await dispatch(
updateUserDetails({ id: user.id, updates })
);
if (updateUserDetails.fulfilled.match(resultAction)) {
toast.success(resultAction.payload.message);
router.refresh();
} else {
toast.error(
(resultAction.payload as string) || "Failed to update user"
);
}
} catch (err) {
console.error("Failed to update user:", err);
toast.error("An unexpected error occurred");
}
};
const loading = status === "loading";
return (
<form className="edit-user" onSubmit={handleSubmit}>
<form className="edit-user" onSubmit={handleUpdate}>
<div className="edit-user__status-toggle">
<FormControlLabel
control={
<Switch
checked={enabled ?? true}
onChange={() => handleUpdate()}
disabled={loading}
color={(enabled ?? true) ? "success" : "default"}
/>
}
label={(enabled ?? true) ? "User Enabled" : "User Disabled"}
/>
</div>
<input
type="text"
placeholder="First Name"
name="firstName"
value={form.firstName}
name="first_name"
value={form.first_name}
onChange={handleChange}
/>
<input
type="text"
placeholder="Last Name"
name="lastName"
value={form.lastName}
name="last_name"
value={form.last_name}
onChange={handleChange}
/>
<input
@ -89,11 +175,104 @@ const EditUser = ({ user }: { user: IUser }) => {
/>
<input
type="text"
placeholder="Role"
name="role"
value={form.role}
placeholder="Username"
name="username"
value={form.username}
onChange={handleChange}
/>
<div className="array-field-container">
<label>Merchants:</label>
<select
name="merchants"
value=""
onChange={handleChange}
className="edit-user__select"
>
<option value="">Select Merchant</option>
{metadataMerchants.map((merchant: string) => (
<option key={merchant} value={merchant}>
{merchant}
</option>
))}
</select>
{form.merchants.length > 0 && (
<div className="selected-items">
{form.merchants.map((merchant, index) => (
<span key={index} className="selected-item">
{merchant}
<button
type="button"
onClick={() => {
setForm(prev => ({
...prev,
merchants: prev.merchants.filter((_, i) => i !== index),
}));
}}
className="remove-item"
>
×
</button>
</span>
))}
</div>
)}
</div>
<div className="array-field-container">
<label>Groups:</label>
<select
name="groups"
value=""
onChange={handleChange}
className="edit-user__select"
>
<option value="">Select Group</option>
{metadataGroups.map((group: string) => (
<option key={group} value={group}>
{group}
</option>
))}
</select>
{form.groups.length > 0 && (
<div className="selected-items">
{form.groups.map((group, index) => (
<span key={index} className="selected-item">
{group}
<button
type="button"
onClick={() => {
setForm(prev => ({
...prev,
groups: prev.groups.filter((_, i) => i !== index),
}));
}}
className="remove-item"
>
×
</button>
</span>
))}
</div>
)}
</div>
<div className="array-field-container">
<label>Job Title:</label>
<select
name="job_title"
value={form.job_title}
onChange={handleChange}
className="edit-user__select"
>
<option value="">Select Job Title</option>
{metadataJobTitles?.map((job_title: string) => (
<option key={job_title} value={job_title}>
{job_title}
</option>
))}
</select>
</div>
<input
type="tel"
placeholder="Phone"
@ -105,10 +284,10 @@ const EditUser = ({ user }: { user: IUser }) => {
inputMode="tel"
autoComplete="tel"
/>
<div className="edit-user__button-container">
<button type="submit">Save</button>
<button type="button" onClick={handleResetForm}>
Clear
<button type="submit" disabled={loading}>
{loading ? "Saving..." : "Save"}
</button>
</div>
</form>

View File

@ -1,22 +1,20 @@
export interface IEditUserForm {
username: string;
firstName: string;
lastName: string;
first_name: string;
last_name: string;
email: string;
role: string;
phone: string;
merchants: string[];
groups: string[];
jobTitle: string;
job_title: string;
}
export type EditUserField =
| "merchants"
| "groups"
| "jobTitle"
| "job_title"
| "username"
| "firstName"
| "lastName"
| "first_name"
| "last_name"
| "email"
| "role"
| "phone";

View File

@ -20,9 +20,9 @@ import {
History,
} from "@mui/icons-material";
import EditUser from "./EditUser/EditUser";
import "./User.scss";
import { IUser } from "../Pages/Admin/Users/interfaces";
import DeleteUser from "./DeleteUser/DeleteUser";
import "./User.scss";
interface Props {
user: IUser;
@ -32,6 +32,7 @@ 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);
};

View File

@ -219,38 +219,6 @@ export const addUser = createAsyncThunk<
}
});
// ---------------- Update User Details ----------------
export const updateUserDetails = createAsyncThunk<
ThunkSuccess<{ user: IUserResponse }>,
{ id: string; updates: Partial<IUserResponse> },
{ rejectValue: ThunkError }
>("auth/updateUserDetails", 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 details");
}
return {
message: data.message || "User details updated successfully",
user: data.user,
};
} catch (err: unknown) {
return rejectWithValue(
(err as Error).message || "Network error during user update"
);
}
});
// ---------------- Slice ----------------
const authSlice = createSlice({
name: "auth",
@ -359,21 +327,6 @@ const authSlice = createSlice({
state.error = action.payload as string;
state.authMessage = action.payload as string;
state.addedUser = null;
})
// Update User Details
.addCase(updateUserDetails.pending, state => {
state.status = "loading";
state.authMessage = "Updating user details...";
})
.addCase(updateUserDetails.fulfilled, (state, action) => {
state.status = "succeeded";
state.user = action.payload.user;
state.authMessage = action.payload.message;
})
.addCase(updateUserDetails.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload as string;
state.authMessage = action.payload as string;
});
},
});

View File

@ -63,7 +63,7 @@ export const editUser = createAsyncThunk<
>("user/editUser", async ({ id, updates }, { rejectWithValue }) => {
try {
const res = await fetch(`/api/dashboard/admin/users/${id}`, {
method: "PATCH",
method: "PUT",
headers: {
"Content-Type": "application/json",
},
@ -87,6 +87,38 @@ export const editUser = createAsyncThunk<
}
});
// ---------------- Update User Details ----------------
export const updateUserDetails = createAsyncThunk<
ThunkSuccess<{ user: IUserResponse }>,
{ id: string; updates: Record<string, unknown> },
{ rejectValue: ThunkError }
>("user/updateUserDetails", async ({ id, updates }, { rejectWithValue }) => {
try {
const res = await fetch(`/api/dashboard/admin/users/${id}`, {
method: "PUT",
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 details");
}
return {
message: data.message || "User details 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 }>,
@ -176,6 +208,25 @@ const userSlice = createSlice({
state.message = action.payload as string;
state.editedUser = null;
})
// Update User Details
.addCase(updateUserDetails.pending, state => {
state.status = "loading";
state.message = "Updating user details...";
state.editedUser = null;
state.error = null;
})
.addCase(updateUserDetails.fulfilled, (state, action) => {
state.status = "succeeded";
state.message = action.payload.message;
state.editedUser = action.payload.user;
state.error = null;
})
.addCase(updateUserDetails.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";