Added New User Registration Detail Modal
This commit is contained in:
parent
fe6ed86a76
commit
be1df88e75
@ -1,9 +1,9 @@
|
|||||||
import Users from "@/app/features/Pages/Admin/Users/users";
|
import Users from "@/app/features/Pages/Admin/Users/users";
|
||||||
|
|
||||||
export default async function BackOfficeUsersPage() {
|
export default async function BackOfficeUsersPage() {
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL
|
// const baseUrl = process.env.NEXT_PUBLIC_BASE_URL
|
||||||
? `${process.env.NEXT_PUBLIC_BASE_URL}`
|
// ? `${process.env.NEXT_PUBLIC_BASE_URL}`
|
||||||
: "http://localhost:4000";
|
// : "http://localhost:4000";
|
||||||
// const res = await fetch(`${baseUrl}/api/dashboard/admin/users`, {
|
// const res = await fetch(`${baseUrl}/api/dashboard/admin/users`, {
|
||||||
// cache: "no-store", // 👈 disables caching for SSR freshness
|
// cache: "no-store", // 👈 disables caching for SSR freshness
|
||||||
// });
|
// });
|
||||||
|
|||||||
@ -3,9 +3,11 @@ import React, { useState } from "react";
|
|||||||
import { Card, CardContent, Typography, Stack } from "@mui/material";
|
import { Card, CardContent, Typography, Stack } from "@mui/material";
|
||||||
import { IUser } from "./interfaces";
|
import { IUser } from "./interfaces";
|
||||||
import UserTopBar from "@/app/features/UserRoles/AddUser/AddUserButton";
|
import UserTopBar from "@/app/features/UserRoles/AddUser/AddUserButton";
|
||||||
import Modal from "@/app/components/Modal/Modal";
|
|
||||||
import UserRoleCard from "@/app/features/UserRoles/userRoleCard";
|
import UserRoleCard from "@/app/features/UserRoles/userRoleCard";
|
||||||
import AddUser from "@/app/features/UserRoles/AddUser/AddUser";
|
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 {
|
interface UsersProps {
|
||||||
users: IUser[];
|
users: IUser[];
|
||||||
@ -13,37 +15,38 @@ interface UsersProps {
|
|||||||
|
|
||||||
const Users: React.FC<UsersProps> = ({ users }) => {
|
const Users: React.FC<UsersProps> = ({ users }) => {
|
||||||
const [showAddUser, setShowAddUser] = useState(false);
|
const [showAddUser, setShowAddUser] = useState(false);
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<UserTopBar onAddUser={() => setShowAddUser(true)} />
|
<UserTopBar
|
||||||
{users.map((user: IUser) => (
|
onAddUser={() => {
|
||||||
<Card key={user.id} sx={{ mb: 2 }}>
|
setShowAddUser(true);
|
||||||
<CardContent>
|
dispatch(setSidebarOpen(false));
|
||||||
<Typography variant="h6">{user.username}</Typography>
|
}}
|
||||||
<Typography variant="body2">
|
/>
|
||||||
Merchant ID: {user.merchantId}
|
{users?.length > 0 &&
|
||||||
</Typography>
|
users.map((user: IUser) => (
|
||||||
|
<Card key={user.id} sx={{ mb: 2 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6">{user.username}</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Merchant ID: {user.merchantId}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<Stack direction="row" spacing={1} mt={1}>
|
<Stack direction="row" spacing={1} mt={1}>
|
||||||
<UserRoleCard
|
<UserRoleCard
|
||||||
user={user}
|
user={user}
|
||||||
isAdmin={true}
|
isAdmin={true}
|
||||||
lastLogin="small"
|
lastLogin="small"
|
||||||
merchants={[]} // merchants={Numberuser.allowedMerchantIds}
|
merchants={[]} // merchants={Numberuser.allowedMerchantIds}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Modal
|
<AddUser open={showAddUser} onClose={() => setShowAddUser(false)} />
|
||||||
open={showAddUser}
|
|
||||||
onClose={() => setShowAddUser(false)}
|
|
||||||
title="Add User"
|
|
||||||
>
|
|
||||||
<AddUser />
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,18 +11,20 @@ import { COUNTRY_CODES } from "../constants";
|
|||||||
import { formatPhoneDisplay, validatePhone } from "../utils";
|
import { formatPhoneDisplay, validatePhone } from "../utils";
|
||||||
import Spinner from "../../../components/Spinner/Spinner";
|
import Spinner from "../../../components/Spinner/Spinner";
|
||||||
import { RootState } from "@/app/redux/store";
|
import { RootState } from "@/app/redux/store";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import Modal from "@/app/components/Modal/Modal";
|
||||||
|
|
||||||
interface AddUserFormProps {
|
interface AddUserProps {
|
||||||
onSuccess?: () => void;
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddUserForm: React.FC<AddUserFormProps> = ({ onSuccess }) => {
|
const AddUser: React.FC<AddUserProps> = ({ open, onClose }) => {
|
||||||
const router = useRouter();
|
// const router = useRouter();
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const { status, error: authError } = useSelector(
|
const { status, error: authError } = useSelector(
|
||||||
(state: RootState) => state.auth
|
(state: RootState) => state.auth
|
||||||
);
|
);
|
||||||
|
|
||||||
const [form, setForm] = useState<IEditUserForm>({
|
const [form, setForm] = useState<IEditUserForm>({
|
||||||
username: "",
|
username: "",
|
||||||
firstName: "",
|
firstName: "",
|
||||||
@ -108,9 +110,14 @@ const AddUserForm: React.FC<AddUserFormProps> = ({ onSuccess }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dispatch(addUser(formattedForm));
|
const resultAction = await dispatch(addUser(formattedForm));
|
||||||
if (onSuccess) onSuccess();
|
const result = addUser.fulfilled.match(resultAction);
|
||||||
router.refresh(); // <- refreshes the page (SSR re-runs)
|
|
||||||
|
if (result && resultAction.payload.success) {
|
||||||
|
toast.success(resultAction.payload.message);
|
||||||
|
// router.refresh();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Error is handled by Redux state
|
// Error is handled by Redux state
|
||||||
console.error("Failed to add user:", err);
|
console.error("Failed to add user:", err);
|
||||||
@ -118,173 +125,175 @@ const AddUserForm: React.FC<AddUserFormProps> = ({ onSuccess }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="add-user" onSubmit={handleSubmit}>
|
<Modal open={open} onClose={onClose} title="Add User">
|
||||||
<input
|
<form className="add-user" onSubmit={handleSubmit}>
|
||||||
name="username"
|
<input
|
||||||
placeholder="Username"
|
name="username"
|
||||||
value={form.username}
|
placeholder="Username"
|
||||||
onChange={handleChange}
|
value={form.username}
|
||||||
required
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
name="firstName"
|
|
||||||
placeholder="First Name"
|
|
||||||
value={form.firstName}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
name="lastName"
|
|
||||||
placeholder="Last Name"
|
|
||||||
value={form.lastName}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="Email"
|
|
||||||
value={form.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<div className="array-field-container">
|
|
||||||
<label>Merchants:</label>
|
|
||||||
<select
|
|
||||||
name="merchants"
|
|
||||||
value=""
|
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="add-user__select"
|
required
|
||||||
>
|
/>
|
||||||
<option value="">Select Merchant</option>
|
<input
|
||||||
<option value="Win Bot">Win Bot</option>
|
name="firstName"
|
||||||
<option value="Data Spin">Data Spin</option>
|
placeholder="First Name"
|
||||||
</select>
|
value={form.firstName}
|
||||||
{form.merchants.length > 0 && (
|
onChange={handleChange}
|
||||||
<div className="selected-items">
|
required
|
||||||
{form.merchants.map((merchant, index) => (
|
/>
|
||||||
<span key={index} className="selected-item">
|
<input
|
||||||
{merchant}
|
name="lastName"
|
||||||
<button
|
placeholder="Last Name"
|
||||||
type="button"
|
value={form.lastName}
|
||||||
onClick={() => {
|
onChange={handleChange}
|
||||||
setForm(prev => ({
|
required
|
||||||
...prev,
|
/>
|
||||||
merchants: prev.merchants.filter((_, i) => i !== index),
|
<input
|
||||||
}));
|
name="email"
|
||||||
}}
|
type="email"
|
||||||
className="remove-item"
|
placeholder="Email"
|
||||||
>
|
value={form.email}
|
||||||
×
|
onChange={handleChange}
|
||||||
</button>
|
required
|
||||||
</span>
|
/>
|
||||||
))}
|
<div className="array-field-container">
|
||||||
</div>
|
<label>Merchants:</label>
|
||||||
)}
|
<select
|
||||||
</div>
|
name="merchants"
|
||||||
|
value=""
|
||||||
|
onChange={handleChange}
|
||||||
|
className="add-user__select"
|
||||||
|
>
|
||||||
|
<option value="">Select Merchant</option>
|
||||||
|
<option value="Win Bot">Win Bot</option>
|
||||||
|
<option value="Data Spin">Data Spin</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">
|
<div className="array-field-container">
|
||||||
<label>Groups:</label>
|
<label>Groups:</label>
|
||||||
|
<select
|
||||||
|
name="groups"
|
||||||
|
value=""
|
||||||
|
onChange={handleChange}
|
||||||
|
className="add-user__select"
|
||||||
|
>
|
||||||
|
<option value="">Select Group</option>
|
||||||
|
<option value="Admin">Admin</option>
|
||||||
|
<option value="Reader">Reader</option>
|
||||||
|
<option value="Manager">Manager</option>
|
||||||
|
<option value="User">User</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>
|
||||||
<select
|
<select
|
||||||
name="groups"
|
name="jobTitle"
|
||||||
value=""
|
value={form.jobTitle}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
required
|
||||||
className="add-user__select"
|
className="add-user__select"
|
||||||
>
|
>
|
||||||
<option value="">Select Group</option>
|
<option value="">Select Job Title</option>
|
||||||
<option value="Admin">Admin</option>
|
<option value="Admin">Admin</option>
|
||||||
<option value="Reader">Reader</option>
|
<option value="Reader">Reader</option>
|
||||||
<option value="Manager">Manager</option>
|
|
||||||
<option value="User">User</option>
|
<option value="User">User</option>
|
||||||
|
<option value="Manager">Manager</option>
|
||||||
|
<option value="Supervisor">Supervisor</option>
|
||||||
|
<option value="Director">Director</option>
|
||||||
</select>
|
</select>
|
||||||
{form.groups.length > 0 && (
|
<div className="phone-input-container">
|
||||||
<div className="selected-items">
|
<select
|
||||||
{form.groups.map((group, index) => (
|
name="countryCode"
|
||||||
<span key={index} className="selected-item">
|
value={countryCode}
|
||||||
{group}
|
onChange={handleCountryCodeChange}
|
||||||
<button
|
className="country-code-select"
|
||||||
type="button"
|
>
|
||||||
onClick={() => {
|
{COUNTRY_CODES.map(country => (
|
||||||
setForm(prev => ({
|
<option key={country.code} value={country.code}>
|
||||||
...prev,
|
{country.flag} {country.code} {country.country}
|
||||||
groups: prev.groups.filter((_, i) => i !== index),
|
</option>
|
||||||
}));
|
|
||||||
}}
|
|
||||||
className="remove-item"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</select>
|
||||||
|
<input
|
||||||
|
name="phone"
|
||||||
|
type="tel"
|
||||||
|
placeholder="Phone number (optional)"
|
||||||
|
value={form.phone}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={phoneError ? "phone-input-error" : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{authError && (
|
||||||
|
<span style={{ color: "red", width: "100%" }}>{authError}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
<span
|
||||||
<select
|
className="phone-error-message"
|
||||||
name="jobTitle"
|
style={{
|
||||||
value={form.jobTitle}
|
visibility: phoneError ? "visible" : "hidden",
|
||||||
onChange={handleChange}
|
color: "red",
|
||||||
required
|
width: "100%",
|
||||||
className="add-user__select"
|
}}
|
||||||
>
|
|
||||||
<option value="">Select Job Title</option>
|
|
||||||
<option value="Admin">Admin</option>
|
|
||||||
<option value="Reader">Reader</option>
|
|
||||||
<option value="User">User</option>
|
|
||||||
<option value="Manager">Manager</option>
|
|
||||||
<option value="Supervisor">Supervisor</option>
|
|
||||||
<option value="Director">Director</option>
|
|
||||||
</select>
|
|
||||||
<div className="phone-input-container">
|
|
||||||
<select
|
|
||||||
name="countryCode"
|
|
||||||
value={countryCode}
|
|
||||||
onChange={handleCountryCodeChange}
|
|
||||||
className="country-code-select"
|
|
||||||
>
|
>
|
||||||
{COUNTRY_CODES.map(country => (
|
{phoneError}
|
||||||
<option key={country.code} value={country.code}>
|
</span>
|
||||||
{country.flag} {country.code} {country.country}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<input
|
|
||||||
name="phone"
|
|
||||||
type="tel"
|
|
||||||
placeholder="Phone number (optional)"
|
|
||||||
value={form.phone}
|
|
||||||
onChange={handleChange}
|
|
||||||
className={phoneError ? "phone-input-error" : ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{authError && (
|
<div className="add-user__button-container">
|
||||||
<span style={{ color: "red", width: "100%" }}>{authError}</span>
|
<button type="submit" disabled={loading}>
|
||||||
)}
|
{loading ? (
|
||||||
<span
|
<>
|
||||||
className="phone-error-message"
|
<Spinner size="small" color="#fff" />
|
||||||
style={{
|
Adding...
|
||||||
visibility: phoneError ? "visible" : "hidden",
|
</>
|
||||||
color: "red",
|
) : (
|
||||||
width: "100%",
|
"Add User"
|
||||||
}}
|
)}
|
||||||
>
|
</button>
|
||||||
{phoneError}
|
</div>
|
||||||
</span>
|
</form>
|
||||||
|
</Modal>
|
||||||
<div className="add-user__button-container">
|
|
||||||
<button type="submit" disabled={loading}>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<Spinner size="small" color="#fff" />
|
|
||||||
Adding...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Add User"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddUserForm;
|
export default AddUser;
|
||||||
|
|||||||
63
app/features/UserRoles/NewUser/NewUser.scss
Normal file
63
app/features/UserRoles/NewUser/NewUser.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/features/UserRoles/NewUser/NewUser.tsx
Normal file
43
app/features/UserRoles/NewUser/NewUser.tsx
Normal file
@ -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<AppDispatch>();
|
||||||
|
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 (
|
||||||
|
<Modal open={true} onClose={handleClose} title="New user Details">
|
||||||
|
<div className="new-user__content">
|
||||||
|
<div className="new-user__row">
|
||||||
|
<label className="new-user__label">Email</label>
|
||||||
|
<div className="new-user__value">{user?.email}</div>
|
||||||
|
</div>
|
||||||
|
<div className="new-user__row">
|
||||||
|
<label className="new-user__label">Temporary Password</label>
|
||||||
|
<div className="new-user__value">
|
||||||
|
<code>{user?.password}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewUser;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
|
import { RootState } from "@/app/redux/types";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { RootState } from "../../redux/types";
|
|
||||||
|
|
||||||
interface MainContentProps {
|
interface MainContentProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import ReduxProvider from "./ReduxProvider"; // moved into app/
|
|||||||
import { AuthBootstrap } from "./AuthBootstrap"; // moved into app/
|
import { AuthBootstrap } from "./AuthBootstrap"; // moved into app/
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import "../styles/globals.scss";
|
import "../styles/globals.scss";
|
||||||
|
import Modals from "./modals";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Your App",
|
title: "Your App",
|
||||||
@ -21,6 +22,7 @@ export default function RootLayout({
|
|||||||
<body>
|
<body>
|
||||||
<ReduxProvider>
|
<ReduxProvider>
|
||||||
{/* Bootstraps session validation + redirect if invalid */}
|
{/* Bootstraps session validation + redirect if invalid */}
|
||||||
|
<Modals />
|
||||||
<AuthBootstrap />
|
<AuthBootstrap />
|
||||||
<ThemeRegistry>{children}</ThemeRegistry>
|
<ThemeRegistry>{children}</ThemeRegistry>
|
||||||
<Toaster position="top-right" reverseOrder={false} />
|
<Toaster position="top-right" reverseOrder={false} />
|
||||||
|
|||||||
14
app/modals.tsx
Normal file
14
app/modals.tsx
Normal file
@ -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 && <NewUser />}</>;
|
||||||
|
};
|
||||||
|
export default Modals;
|
||||||
@ -18,6 +18,7 @@ interface AuthState {
|
|||||||
user: IUserResponse | null;
|
user: IUserResponse | null;
|
||||||
tokenInfo: TokenInfo | null;
|
tokenInfo: TokenInfo | null;
|
||||||
mustChangePassword: boolean;
|
mustChangePassword: boolean;
|
||||||
|
addedUser: IUserResponse | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
@ -28,6 +29,7 @@ const initialState: AuthState = {
|
|||||||
user: null,
|
user: null,
|
||||||
tokenInfo: null,
|
tokenInfo: null,
|
||||||
mustChangePassword: false,
|
mustChangePassword: false,
|
||||||
|
addedUser: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------- Login ----------------
|
// ---------------- Login ----------------
|
||||||
@ -179,7 +181,7 @@ export const validateAuth = createAsyncThunk<
|
|||||||
|
|
||||||
// ---------------- Add User ----------------
|
// ---------------- Add User ----------------
|
||||||
export const addUser = createAsyncThunk<
|
export const addUser = createAsyncThunk<
|
||||||
ThunkSuccess<{ user: IUserResponse }>,
|
ThunkSuccess<{ user: IUserResponse; success: boolean }>,
|
||||||
IEditUserForm,
|
IEditUserForm,
|
||||||
{ rejectValue: ThunkError; state: RootState }
|
{ rejectValue: ThunkError; state: RootState }
|
||||||
>("auth/addUser", async (userData, { rejectWithValue, getState }) => {
|
>("auth/addUser", async (userData, { rejectWithValue, getState }) => {
|
||||||
@ -195,14 +197,17 @@ export const addUser = createAsyncThunk<
|
|||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
|
console.log("[DEBUG] [ADD-USER] [data]: ", data);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return rejectWithValue(data.message || "Failed to create user");
|
return rejectWithValue(data.message || "Failed to create user");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
success: data.success,
|
||||||
message: data.message || "User created successfully",
|
message: data.message || "User created successfully",
|
||||||
user: data.user,
|
user: data.user,
|
||||||
};
|
} as ThunkSuccess<{ user: IUserResponse; success: boolean }>;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return rejectWithValue(
|
return rejectWithValue(
|
||||||
(err as Error).message || "Network error during user creation"
|
(err as Error).message || "Network error during user creation"
|
||||||
@ -253,6 +258,9 @@ const authSlice = createSlice({
|
|||||||
clearAuthMessage: state => {
|
clearAuthMessage: state => {
|
||||||
state.authMessage = "";
|
state.authMessage = "";
|
||||||
},
|
},
|
||||||
|
clearAddedUser: state => {
|
||||||
|
state.addedUser = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: builder => {
|
extraReducers: builder => {
|
||||||
builder
|
builder
|
||||||
@ -277,12 +285,11 @@ const authSlice = createSlice({
|
|||||||
})
|
})
|
||||||
// Logout
|
// Logout
|
||||||
.addCase(logout.fulfilled, (state, action) => {
|
.addCase(logout.fulfilled, (state, action) => {
|
||||||
state.status = "succeeded";
|
return {
|
||||||
state.isLoggedIn = false;
|
...initialState,
|
||||||
state.authMessage = action.payload.message;
|
status: "succeeded",
|
||||||
state.user = null;
|
authMessage: action.payload.message,
|
||||||
state.tokenInfo = null;
|
};
|
||||||
state.mustChangePassword = false;
|
|
||||||
})
|
})
|
||||||
.addCase(logout.rejected, (state, action) => {
|
.addCase(logout.rejected, (state, action) => {
|
||||||
state.status = "failed";
|
state.status = "failed";
|
||||||
@ -291,12 +298,12 @@ const authSlice = createSlice({
|
|||||||
})
|
})
|
||||||
// Auto Logout
|
// Auto Logout
|
||||||
.addCase(autoLogout.fulfilled, (state, action) => {
|
.addCase(autoLogout.fulfilled, (state, action) => {
|
||||||
state.status = "succeeded";
|
return {
|
||||||
state.isLoggedIn = false;
|
...initialState,
|
||||||
state.authMessage = action.payload.message;
|
status: "succeeded",
|
||||||
state.user = null;
|
isLoggedIn: false,
|
||||||
state.tokenInfo = null;
|
authMessage: action.payload.message,
|
||||||
state.mustChangePassword = false;
|
};
|
||||||
})
|
})
|
||||||
.addCase(autoLogout.rejected, (state, action) => {
|
.addCase(autoLogout.rejected, (state, action) => {
|
||||||
state.status = "failed";
|
state.status = "failed";
|
||||||
@ -336,15 +343,18 @@ const authSlice = createSlice({
|
|||||||
.addCase(addUser.pending, state => {
|
.addCase(addUser.pending, state => {
|
||||||
state.status = "loading";
|
state.status = "loading";
|
||||||
state.authMessage = "Creating user...";
|
state.authMessage = "Creating user...";
|
||||||
|
state.addedUser = null;
|
||||||
})
|
})
|
||||||
.addCase(addUser.fulfilled, (state, action) => {
|
.addCase(addUser.fulfilled, (state, action) => {
|
||||||
state.status = "succeeded";
|
state.status = "succeeded";
|
||||||
state.authMessage = action.payload.message;
|
state.authMessage = action.payload.message;
|
||||||
|
state.addedUser = action.payload.user;
|
||||||
})
|
})
|
||||||
.addCase(addUser.rejected, (state, action) => {
|
.addCase(addUser.rejected, (state, action) => {
|
||||||
state.status = "failed";
|
state.status = "failed";
|
||||||
state.error = action.payload as string;
|
state.error = action.payload as string;
|
||||||
state.authMessage = action.payload as string;
|
state.authMessage = action.payload as string;
|
||||||
|
state.addedUser = null;
|
||||||
})
|
})
|
||||||
// Update User Details
|
// Update User Details
|
||||||
.addCase(updateUserDetails.pending, state => {
|
.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;
|
export default authSlice.reducer;
|
||||||
|
|||||||
@ -12,3 +12,4 @@ export const selectTimeUntilExpiration = (state: RootState) =>
|
|||||||
state.auth?.tokenInfo?.timeUntilExpiration || 0;
|
state.auth?.tokenInfo?.timeUntilExpiration || 0;
|
||||||
export const selectExpiresInHours = (state: RootState) =>
|
export const selectExpiresInHours = (state: RootState) =>
|
||||||
state.auth?.tokenInfo?.expiresInHours || 0;
|
state.auth?.tokenInfo?.expiresInHours || 0;
|
||||||
|
export const selectAddedUser = (state: RootState) => state.auth?.addedUser;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import authReducer from "./auth/authSlice";
|
|||||||
import uiReducer from "./ui/uiSlice";
|
import uiReducer from "./ui/uiSlice";
|
||||||
import userEpics from "./user/epic";
|
import userEpics from "./user/epic";
|
||||||
import authEpics from "./auth/epic";
|
import authEpics from "./auth/epic";
|
||||||
|
import uiEpics from "./ui/epic";
|
||||||
|
|
||||||
type PersistedAuth = { user: unknown | null };
|
type PersistedAuth = { user: unknown | null };
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ const rootReducer = combineReducers({
|
|||||||
ui: uiReducer,
|
ui: uiReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootEpic = combineEpics(...userEpics, ...authEpics);
|
const rootEpic = combineEpics(...userEpics, ...authEpics, ...uiEpics);
|
||||||
|
|
||||||
const persistedReducer = persistReducer(persistConfig, rootReducer as Reducer);
|
const persistedReducer = persistReducer(persistConfig, rootReducer as Reducer);
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export type ThunkSuccess<T extends object = object> = { message: string } & T;
|
|||||||
export type ThunkError = string;
|
export type ThunkError = string;
|
||||||
|
|
||||||
export interface IUserResponse {
|
export interface IUserResponse {
|
||||||
|
success: boolean;
|
||||||
token: string;
|
token: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
@ -19,6 +20,7 @@ export interface IUserResponse {
|
|||||||
phone?: string | null;
|
phone?: string | null;
|
||||||
jobTitle?: string | null;
|
jobTitle?: string | null;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
password?: string;
|
||||||
authorities?: string[];
|
authorities?: string[];
|
||||||
allowedMerchantIds?: number[];
|
allowedMerchantIds?: number[];
|
||||||
created?: string;
|
created?: string;
|
||||||
@ -40,5 +42,5 @@ export interface ILoginResponse {
|
|||||||
user: IUserResponse;
|
user: IUserResponse;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
must_change_password: boolean;
|
must_change_password?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
16
app/redux/ui/epic.ts
Normal file
16
app/redux/ui/epic.ts
Normal file
@ -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;
|
||||||
4
app/redux/ui/selectors.ts
Normal file
4
app/redux/ui/selectors.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { RootState } from "../types";
|
||||||
|
|
||||||
|
export const selectShowNewUserModal = (state: RootState) =>
|
||||||
|
state.ui.showNewUserModal;
|
||||||
@ -2,10 +2,12 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|||||||
|
|
||||||
interface UIState {
|
interface UIState {
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
|
showNewUserModal: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: UIState = {
|
const initialState: UIState = {
|
||||||
sidebarOpen: true,
|
sidebarOpen: true,
|
||||||
|
showNewUserModal: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const uiSlice = createSlice({
|
const uiSlice = createSlice({
|
||||||
@ -18,8 +20,12 @@ const uiSlice = createSlice({
|
|||||||
setSidebarOpen: (state, action: PayloadAction<boolean>) => {
|
setSidebarOpen: (state, action: PayloadAction<boolean>) => {
|
||||||
state.sidebarOpen = action.payload;
|
state.sidebarOpen = action.payload;
|
||||||
},
|
},
|
||||||
|
setShowNewUserModal: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.showNewUserModal = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { toggleSidebar, setSidebarOpen } = uiSlice.actions;
|
export const { toggleSidebar, setSidebarOpen, setShowNewUserModal } =
|
||||||
|
uiSlice.actions;
|
||||||
export default uiSlice.reducer;
|
export default uiSlice.reducer;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user