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[]; groups?: string[];
merchants?: string[]; merchants?: string[];
creator?: string; creator?: string;
countryCode: string;
} }
export async function POST(request: Request) { export async function POST(request: Request) {

View File

@ -17,7 +17,7 @@
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2);
position: relative; position: relative;
min-width: 320px; min-width: 320px;
max-width: 60vw; max-width: 65vw;
max-height: 90vh; max-height: 90vh;
overflow: auto; overflow: auto;
padding: 2rem 1.5rem 1.5rem 1.5rem; 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; border-radius: 4px;
width: 100px; width: 100px;
cursor: pointer; cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
} }
button:first-child { button:first-child {

View File

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

View File

@ -1,5 +1,7 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { ThunkSuccess, ThunkError, IUserResponse } from "../types"; import { ThunkSuccess, ThunkError, IUserResponse } from "../types";
import { IEditUserForm } from "../../features/UserRoles/User.interfaces";
import { RootState } from "../store";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
interface TokenInfo { interface TokenInfo {
@ -175,6 +177,39 @@ export const validateAuth = createAsyncThunk<
// TODO - Creaye a new thunk to update the user stuff // 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 ---------------- // ---------------- Update User Details ----------------
export const updateUserDetails = createAsyncThunk< export const updateUserDetails = createAsyncThunk<
ThunkSuccess<{ user: IUserResponse }>, ThunkSuccess<{ user: IUserResponse }>,
@ -297,6 +332,20 @@ const authSlice = createSlice({
state.tokenInfo = null; state.tokenInfo = null;
state.error = action.payload as string; 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 // Update User Details
.addCase(updateUserDetails.pending, state => { .addCase(updateUserDetails.pending, state => {
state.status = "loading"; state.status = "loading";

View File

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