Fixed user edit and introduced user enabled/disabled
This commit is contained in:
parent
568f56bf00
commit
a3ad2a8937
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
// 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] };
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetForm = () => {
|
||||
setForm({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
role: "",
|
||||
phone: "",
|
||||
groups: [],
|
||||
merchants: [],
|
||||
jobTitle: "",
|
||||
username: "",
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
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");
|
||||
}
|
||||
} else {
|
||||
// Handle single value fields
|
||||
setForm(prev => ({ ...prev, [name]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
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>
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user