diff --git a/app/dashboard/admin/users/page.tsx b/app/dashboard/admin/users/page.tsx index 68def4b..d72a71b 100644 --- a/app/dashboard/admin/users/page.tsx +++ b/app/dashboard/admin/users/page.tsx @@ -1,9 +1,9 @@ import Users from "@/app/features/Pages/Admin/Users/users"; export default async function BackOfficeUsersPage() { - const baseUrl = process.env.NEXT_PUBLIC_BASE_URL - ? `${process.env.NEXT_PUBLIC_BASE_URL}` - : "http://localhost:4000"; + // const baseUrl = process.env.NEXT_PUBLIC_BASE_URL + // ? `${process.env.NEXT_PUBLIC_BASE_URL}` + // : "http://localhost:4000"; // const res = await fetch(`${baseUrl}/api/dashboard/admin/users`, { // cache: "no-store", // 👈 disables caching for SSR freshness // }); diff --git a/app/features/Pages/Admin/Users/users.tsx b/app/features/Pages/Admin/Users/users.tsx index 0ef78f7..03d62ec 100644 --- a/app/features/Pages/Admin/Users/users.tsx +++ b/app/features/Pages/Admin/Users/users.tsx @@ -3,9 +3,11 @@ import React, { useState } from "react"; import { Card, CardContent, Typography, Stack } from "@mui/material"; import { IUser } from "./interfaces"; import UserTopBar from "@/app/features/UserRoles/AddUser/AddUserButton"; -import Modal from "@/app/components/Modal/Modal"; import UserRoleCard from "@/app/features/UserRoles/userRoleCard"; import AddUser from "@/app/features/UserRoles/AddUser/AddUser"; +import { setSidebarOpen } from "@/app/redux/ui/uiSlice"; +import { AppDispatch } from "@/app/redux/types"; +import { useDispatch } from "react-redux"; interface UsersProps { users: IUser[]; @@ -13,37 +15,38 @@ interface UsersProps { const Users: React.FC = ({ users }) => { const [showAddUser, setShowAddUser] = useState(false); + const dispatch = useDispatch(); return (
- setShowAddUser(true)} /> - {users.map((user: IUser) => ( - - - {user.username} - - Merchant ID: {user.merchantId} - + { + setShowAddUser(true); + dispatch(setSidebarOpen(false)); + }} + /> + {users?.length > 0 && + users.map((user: IUser) => ( + + + {user.username} + + Merchant ID: {user.merchantId} + - - - - - - ))} + + + + + + ))} - setShowAddUser(false)} - title="Add User" - > - - + setShowAddUser(false)} />
); }; diff --git a/app/features/UserRoles/AddUser/AddUser.tsx b/app/features/UserRoles/AddUser/AddUser.tsx index 8a643e3..fc0c146 100644 --- a/app/features/UserRoles/AddUser/AddUser.tsx +++ b/app/features/UserRoles/AddUser/AddUser.tsx @@ -11,18 +11,20 @@ import { COUNTRY_CODES } from "../constants"; import { formatPhoneDisplay, validatePhone } from "../utils"; import Spinner from "../../../components/Spinner/Spinner"; import { RootState } from "@/app/redux/store"; +import toast from "react-hot-toast"; +import Modal from "@/app/components/Modal/Modal"; -interface AddUserFormProps { - onSuccess?: () => void; +interface AddUserProps { + open: boolean; + onClose: () => void; } -const AddUserForm: React.FC = ({ onSuccess }) => { - const router = useRouter(); +const AddUser: React.FC = ({ open, onClose }) => { + // const router = useRouter(); const dispatch = useDispatch(); const { status, error: authError } = useSelector( (state: RootState) => state.auth ); - const [form, setForm] = useState({ username: "", firstName: "", @@ -108,9 +110,14 @@ const AddUserForm: React.FC = ({ onSuccess }) => { }; try { - await dispatch(addUser(formattedForm)); - if (onSuccess) onSuccess(); - router.refresh(); // <- refreshes the page (SSR re-runs) + const resultAction = await dispatch(addUser(formattedForm)); + const result = addUser.fulfilled.match(resultAction); + + if (result && resultAction.payload.success) { + toast.success(resultAction.payload.message); + // router.refresh(); + onClose(); + } } catch (err) { // Error is handled by Redux state console.error("Failed to add user:", err); @@ -118,173 +125,175 @@ const AddUserForm: React.FC = ({ onSuccess }) => { }; return ( -
- - - - -
- - - - - - - {form.merchants.length > 0 && ( -
- {form.merchants.map((merchant, index) => ( - - {merchant} - - - ))} -
- )} -
+ required + /> + + + +
+ + + {form.merchants.length > 0 && ( +
+ {form.merchants.map((merchant, index) => ( + + {merchant} + + + ))} +
+ )} +
-
- +
+ + + {form.groups.length > 0 && ( +
+ {form.groups.map((group, index) => ( + + {group} + + + ))} +
+ )} +
- {form.groups.length > 0 && ( -
- {form.groups.map((group, index) => ( - - {group} - - +
+ + +
+ + {authError && ( + {authError} )} -
- -
- - -
+ {phoneError} + - {authError && ( - {authError} - )} - - {phoneError} - - -
- -
- +
+ +
+ + ); }; -export default AddUserForm; +export default AddUser; diff --git a/app/features/UserRoles/NewUser/NewUser.scss b/app/features/UserRoles/NewUser/NewUser.scss new file mode 100644 index 0000000..bd30a5c --- /dev/null +++ b/app/features/UserRoles/NewUser/NewUser.scss @@ -0,0 +1,63 @@ +.new-user__content { + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 1rem 0; + + .new-user__row { + display: flex; + flex-direction: column; + gap: 0.4rem; + + .new-user__label { + font-weight: 600; + color: #444; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .new-user__value { + background: #f7f7f8; + border: 1px solid #e0e0e0; + border-radius: 6px; + padding: 0.75rem 1rem; + font-size: 0.95rem; + color: #222; + word-break: break-word; + + code { + background: #272822; + color: #f8f8f2; + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-family: "Source Code Pro", monospace; + font-size: 0.9rem; + } + } + } +} + +/* Optional: Add a subtle hover highlight for readability */ +.new-user__row:hover .new-user__value { + border-color: #bdbdbd; + background: #fafafa; +} + +/* Responsive tweak for smaller screens */ +@media (max-width: 600px) { + .new-user__content { + gap: 1rem; + } + + .new-user__row { + .new-user__label { + font-size: 0.85rem; + } + + .new-user__value { + font-size: 0.9rem; + padding: 0.6rem 0.8rem; + } + } +} diff --git a/app/features/UserRoles/NewUser/NewUser.tsx b/app/features/UserRoles/NewUser/NewUser.tsx new file mode 100644 index 0000000..bb14f91 --- /dev/null +++ b/app/features/UserRoles/NewUser/NewUser.tsx @@ -0,0 +1,43 @@ +"use client"; + +import React from "react"; +import Modal from "@/app/components/Modal/Modal"; +import { useDispatch, useSelector } from "react-redux"; +import { clearAddedUser } from "@/app/redux/auth/authSlice"; +import { AppDispatch } from "@/app/redux/types"; +import { useRouter } from "next/navigation"; +import { selectAddedUser } from "@/app/redux/auth/selectors"; +import { setShowNewUserModal } from "@/app/redux/ui/uiSlice"; +import "./NewUser.scss"; + +const NewUser: React.FC = () => { + const dispatch = useDispatch(); + const router = useRouter(); + const user = useSelector(selectAddedUser); + + const handleClose = () => { + dispatch(clearAddedUser()); + dispatch(setShowNewUserModal(false)); + // Refresh data after closing to update lists without racing rehydrate + router.refresh(); + }; + + return ( + +
+
+ +
{user?.email}
+
+
+ +
+ {user?.password} +
+
+
+
+ ); +}; + +export default NewUser; diff --git a/app/features/dashboard/layout/mainContent.tsx b/app/features/dashboard/layout/mainContent.tsx index d318b8f..1f55fd3 100644 --- a/app/features/dashboard/layout/mainContent.tsx +++ b/app/features/dashboard/layout/mainContent.tsx @@ -1,6 +1,6 @@ +import { RootState } from "@/app/redux/types"; import React from "react"; import { useSelector } from "react-redux"; -import { RootState } from "../../redux/types"; interface MainContentProps { children: React.ReactNode; diff --git a/app/layout.tsx b/app/layout.tsx index eb2891a..cfa4e7b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,7 @@ import ReduxProvider from "./ReduxProvider"; // moved into app/ import { AuthBootstrap } from "./AuthBootstrap"; // moved into app/ import { Toaster } from "react-hot-toast"; import "../styles/globals.scss"; +import Modals from "./modals"; export const metadata: Metadata = { title: "Your App", @@ -21,6 +22,7 @@ export default function RootLayout({ {/* Bootstraps session validation + redirect if invalid */} + {children} diff --git a/app/modals.tsx b/app/modals.tsx new file mode 100644 index 0000000..eec8c76 --- /dev/null +++ b/app/modals.tsx @@ -0,0 +1,14 @@ +"use client"; +import React from "react"; +import NewUser from "./features/UserRoles/NewUser/NewUser"; +import { useSelector } from "react-redux"; +// import { selectIsLoggedIn } from "./redux/auth/selectors"; +import { selectShowNewUserModal } from "./redux/ui/selectors"; + +const Modals: React.FC = () => { + // const isLoggedIn = useSelector(selectIsLoggedIn); + const showNewUserModal = useSelector(selectShowNewUserModal); + + return <>{showNewUserModal && }; +}; +export default Modals; diff --git a/app/redux/auth/authSlice.tsx b/app/redux/auth/authSlice.tsx index 5fe1e19..56b8f13 100644 --- a/app/redux/auth/authSlice.tsx +++ b/app/redux/auth/authSlice.tsx @@ -18,6 +18,7 @@ interface AuthState { user: IUserResponse | null; tokenInfo: TokenInfo | null; mustChangePassword: boolean; + addedUser: IUserResponse | null; } const initialState: AuthState = { @@ -28,6 +29,7 @@ const initialState: AuthState = { user: null, tokenInfo: null, mustChangePassword: false, + addedUser: null, }; // ---------------- Login ---------------- @@ -179,7 +181,7 @@ export const validateAuth = createAsyncThunk< // ---------------- Add User ---------------- export const addUser = createAsyncThunk< - ThunkSuccess<{ user: IUserResponse }>, + ThunkSuccess<{ user: IUserResponse; success: boolean }>, IEditUserForm, { rejectValue: ThunkError; state: RootState } >("auth/addUser", async (userData, { rejectWithValue, getState }) => { @@ -195,14 +197,17 @@ export const addUser = createAsyncThunk< 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" @@ -253,6 +258,9 @@ const authSlice = createSlice({ clearAuthMessage: state => { state.authMessage = ""; }, + clearAddedUser: state => { + state.addedUser = null; + }, }, extraReducers: builder => { builder @@ -277,12 +285,11 @@ const authSlice = createSlice({ }) // Logout .addCase(logout.fulfilled, (state, action) => { - state.status = "succeeded"; - state.isLoggedIn = false; - state.authMessage = action.payload.message; - state.user = null; - state.tokenInfo = null; - state.mustChangePassword = false; + return { + ...initialState, + status: "succeeded", + authMessage: action.payload.message, + }; }) .addCase(logout.rejected, (state, action) => { state.status = "failed"; @@ -291,12 +298,12 @@ const authSlice = createSlice({ }) // Auto Logout .addCase(autoLogout.fulfilled, (state, action) => { - state.status = "succeeded"; - state.isLoggedIn = false; - state.authMessage = action.payload.message; - state.user = null; - state.tokenInfo = null; - state.mustChangePassword = false; + return { + ...initialState, + status: "succeeded", + isLoggedIn: false, + authMessage: action.payload.message, + }; }) .addCase(autoLogout.rejected, (state, action) => { state.status = "failed"; @@ -336,15 +343,18 @@ const authSlice = createSlice({ .addCase(addUser.pending, state => { state.status = "loading"; state.authMessage = "Creating user..."; + state.addedUser = null; }) .addCase(addUser.fulfilled, (state, action) => { state.status = "succeeded"; state.authMessage = action.payload.message; + state.addedUser = action.payload.user; }) .addCase(addUser.rejected, (state, action) => { state.status = "failed"; state.error = action.payload as string; state.authMessage = action.payload as string; + state.addedUser = null; }) // Update User Details .addCase(updateUserDetails.pending, state => { @@ -364,5 +374,6 @@ const authSlice = createSlice({ }, }); -export const { setAuthMessage, clearAuthMessage } = authSlice.actions; +export const { setAuthMessage, clearAuthMessage, clearAddedUser } = + authSlice.actions; export default authSlice.reducer; diff --git a/app/redux/auth/selectors.ts b/app/redux/auth/selectors.ts index abeb76d..e4ab213 100644 --- a/app/redux/auth/selectors.ts +++ b/app/redux/auth/selectors.ts @@ -12,3 +12,4 @@ export const selectTimeUntilExpiration = (state: RootState) => state.auth?.tokenInfo?.timeUntilExpiration || 0; export const selectExpiresInHours = (state: RootState) => state.auth?.tokenInfo?.expiresInHours || 0; +export const selectAddedUser = (state: RootState) => state.auth?.addedUser; diff --git a/app/redux/store.ts b/app/redux/store.ts index fd5e829..c1a76b6 100644 --- a/app/redux/store.ts +++ b/app/redux/store.ts @@ -11,6 +11,7 @@ import authReducer from "./auth/authSlice"; import uiReducer from "./ui/uiSlice"; import userEpics from "./user/epic"; import authEpics from "./auth/epic"; +import uiEpics from "./ui/epic"; type PersistedAuth = { user: unknown | null }; @@ -40,7 +41,7 @@ const rootReducer = combineReducers({ ui: uiReducer, }); -const rootEpic = combineEpics(...userEpics, ...authEpics); +const rootEpic = combineEpics(...userEpics, ...authEpics, ...uiEpics); const persistedReducer = persistReducer(persistConfig, rootReducer as Reducer); diff --git a/app/redux/types.ts b/app/redux/types.ts index 473a43a..1194c62 100644 --- a/app/redux/types.ts +++ b/app/redux/types.ts @@ -8,6 +8,7 @@ export type ThunkSuccess = { message: string } & T; export type ThunkError = string; export interface IUserResponse { + success: boolean; token: string; id: string; name: string | null; @@ -19,6 +20,7 @@ export interface IUserResponse { phone?: string | null; jobTitle?: string | null; enabled?: boolean; + password?: string; authorities?: string[]; allowedMerchantIds?: number[]; created?: string; @@ -40,5 +42,5 @@ export interface ILoginResponse { user: IUserResponse; success: boolean; message: string; - must_change_password: boolean; + must_change_password?: boolean; } diff --git a/app/redux/ui/epic.ts b/app/redux/ui/epic.ts new file mode 100644 index 0000000..4804665 --- /dev/null +++ b/app/redux/ui/epic.ts @@ -0,0 +1,16 @@ +import { Epic } from "redux-observable"; +import { filter, map } from "rxjs/operators"; +import { addUser } from "../auth/authSlice"; +import { setShowNewUserModal } from "./uiSlice"; + +export const logoutRedirectEpic: Epic = action$ => + action$.pipe( + filter(addUser.fulfilled.match), + map(() => { + return setShowNewUserModal(true); + }) + ); + +const authEpics = [logoutRedirectEpic]; + +export default authEpics; diff --git a/app/redux/ui/selectors.ts b/app/redux/ui/selectors.ts new file mode 100644 index 0000000..500fb4f --- /dev/null +++ b/app/redux/ui/selectors.ts @@ -0,0 +1,4 @@ +import { RootState } from "../types"; + +export const selectShowNewUserModal = (state: RootState) => + state.ui.showNewUserModal; diff --git a/app/redux/ui/uiSlice.ts b/app/redux/ui/uiSlice.ts index fdb263f..aa18af5 100644 --- a/app/redux/ui/uiSlice.ts +++ b/app/redux/ui/uiSlice.ts @@ -2,10 +2,12 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; interface UIState { sidebarOpen: boolean; + showNewUserModal: boolean; } const initialState: UIState = { sidebarOpen: true, + showNewUserModal: false, }; const uiSlice = createSlice({ @@ -18,8 +20,12 @@ const uiSlice = createSlice({ setSidebarOpen: (state, action: PayloadAction) => { state.sidebarOpen = action.payload; }, + setShowNewUserModal: (state, action: PayloadAction) => { + state.showNewUserModal = action.payload; + }, }, }); -export const { toggleSidebar, setSidebarOpen } = uiSlice.actions; +export const { toggleSidebar, setSidebarOpen, setShowNewUserModal } = + uiSlice.actions; export default uiSlice.reducer;