Fixed creator bug when adding new user

This commit is contained in:
Mitchell Magro 2025-10-27 13:23:00 +01:00
parent 483f2656da
commit eff7f62299
8 changed files with 175 additions and 34 deletions

View File

@ -30,6 +30,7 @@ interface FrontendRegisterForm {
groups?: string[];
merchants?: string[];
creator?: string;
countryCode: string;
}
export async function POST(request: Request) {

View File

@ -17,7 +17,7 @@
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2);
position: relative;
min-width: 320px;
max-width: 60vw;
max-width: 65vw;
max-height: 90vh;
overflow: auto;
padding: 2rem 1.5rem 1.5rem 1.5rem;

View File

@ -0,0 +1,48 @@
.spinner {
display: inline-block;
position: relative;
border-radius: 50%;
border: 2px solid transparent;
border-top: 2px solid #1976d2;
animation: spin 1s linear infinite;
&--small {
width: 16px;
height: 16px;
border-width: 1px;
}
&--medium {
width: 20px;
height: 20px;
border-width: 2px;
}
&--large {
width: 32px;
height: 32px;
border-width: 3px;
}
&__inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60%;
height: 60%;
border-radius: 50%;
border: 1px solid transparent;
border-top: 1px solid currentColor;
animation: spin 0.8s linear infinite reverse;
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,23 @@
import React from "react";
import "./Spinner.scss";
interface SpinnerProps {
size?: "small" | "medium" | "large";
color?: string;
}
const Spinner: React.FC<SpinnerProps> = ({
size = "medium",
color = "#1976d2",
}) => {
return (
<div
className={`spinner spinner--${size}`}
style={{ borderTopColor: color }}
>
<div className="spinner__inner"></div>
</div>
);
};
export default Spinner;

View File

@ -67,6 +67,15 @@
border-radius: 4px;
width: 100px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
}
button:first-child {

View File

@ -2,11 +2,15 @@
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch } from "@/app/redux/store";
import "./AddUser.scss";
import { addUser } from "@/services/roles.services";
import { addUser } from "@/app/redux/auth/authSlice";
import { IEditUserForm } from "../User.interfaces";
import { COUNTRY_CODES } from "../constants";
import { formatPhoneDisplay, validatePhone } from "../utils";
import Spinner from "../../../components/Spinner/Spinner";
import { RootState } from "@/app/redux/store";
interface AddUserFormProps {
onSuccess?: () => void;
@ -14,6 +18,11 @@ interface AddUserFormProps {
const AddUserForm: React.FC<AddUserFormProps> = ({ onSuccess }) => {
const router = useRouter();
const dispatch = useDispatch<AppDispatch>();
const { status, error: authError } = useSelector(
(state: RootState) => state.auth
);
const [form, setForm] = useState<IEditUserForm>({
username: "",
firstName: "",
@ -26,11 +35,11 @@ const AddUserForm: React.FC<AddUserFormProps> = ({ onSuccess }) => {
jobTitle: "",
});
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [phoneError, setPhoneError] = useState("");
const [countryCode, setCountryCode] = useState("+1");
const loading = status === "loading";
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
@ -78,7 +87,6 @@ const AddUserForm: React.FC<AddUserFormProps> = ({ onSuccess }) => {
// Validate phone number if provided
if (form.phone && phoneError) {
setError("Please fix phone number errors before submitting.");
return;
}
@ -90,28 +98,22 @@ const AddUserForm: React.FC<AddUserFormProps> = ({ onSuccess }) => {
form.groups.length === 0 ||
!form.jobTitle
) {
setError("Please fill in all required fields.");
return;
}
try {
setLoading(true);
setError("");
// Format phone number with country code before submission
const formattedForm = {
...form,
phone: form.phone ? formatPhoneDisplay(form.phone, countryCode) : "",
};
await addUser(formattedForm);
try {
await dispatch(addUser(formattedForm));
if (onSuccess) onSuccess();
router.refresh(); // <- refreshes the page (SSR re-runs)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
setError(err.message || "Something went wrong.");
} finally {
setLoading(false);
} catch (err) {
// Error is handled by Redux state
console.error("Failed to add user:", err);
}
};
@ -255,7 +257,9 @@ const AddUserForm: React.FC<AddUserFormProps> = ({ onSuccess }) => {
/>
</div>
{error && <span style={{ color: "red", width: "100%" }}>{error}</span>}
{authError && (
<span style={{ color: "red", width: "100%" }}>{authError}</span>
)}
<span
className="phone-error-message"
style={{
@ -269,7 +273,14 @@ const AddUserForm: React.FC<AddUserFormProps> = ({ onSuccess }) => {
<div className="add-user__button-container">
<button type="submit" disabled={loading}>
{loading ? "Adding..." : "Add User"}
{loading ? (
<>
<Spinner size="small" color="#fff" />
Adding...
</>
) : (
"Add User"
)}
</button>
</div>
</form>

View File

@ -1,5 +1,7 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { ThunkSuccess, ThunkError, IUserResponse } from "../types";
import { IEditUserForm } from "../../features/UserRoles/User.interfaces";
import { RootState } from "../store";
import toast from "react-hot-toast";
interface TokenInfo {
@ -175,6 +177,39 @@ export const validateAuth = createAsyncThunk<
// TODO - Creaye a new thunk to update the user stuff
// ---------------- Add User ----------------
export const addUser = createAsyncThunk<
ThunkSuccess<{ user: IUserResponse }>,
IEditUserForm,
{ rejectValue: ThunkError; state: RootState }
>("auth/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();
if (!res.ok) {
return rejectWithValue(data.message || "Failed to create user");
}
return {
message: data.message || "User created successfully",
user: data.user,
};
} catch (err: unknown) {
return rejectWithValue(
(err as Error).message || "Network error during user creation"
);
}
});
// ---------------- Update User Details ----------------
export const updateUserDetails = createAsyncThunk<
ThunkSuccess<{ user: IUserResponse }>,
@ -297,6 +332,20 @@ const authSlice = createSlice({
state.tokenInfo = null;
state.error = action.payload as string;
})
// Add User
.addCase(addUser.pending, state => {
state.status = "loading";
state.authMessage = "Creating user...";
})
.addCase(addUser.fulfilled, (state, action) => {
state.status = "succeeded";
state.authMessage = action.payload.message;
})
.addCase(addUser.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload as string;
state.authMessage = action.payload as string;
})
// Update User Details
.addCase(updateUserDetails.pending, state => {
state.status = "loading";

View File

@ -1,18 +1,18 @@
import { IEditUserForm } from "@/app/features/UserRoles/User.interfaces";
export async function addUser(data: IEditUserForm) {
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
// export async function addUser(data: IEditUserForm) {
// const res = await fetch("/api/auth/register", {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(data),
// });
if (!res.ok) {
throw new Error("Failed to create role");
}
// if (!res.ok) {
// throw new Error("Failed to create role");
// }
return res.json(); // or return type depending on your backend
}
// return res.json(); // or return type depending on your backend
// }
export async function editUser(id: string, data: IEditUserForm) {
console.log("[editUser] - id", id, data);
const res = await fetch(`/api/dashboard/admin/users/${id}`, {