@visns-studio/visns-components 5.5.5 → 5.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -0
- package/package.json +1 -1
- package/src/components/crm/auth/ClientLogin.jsx +126 -0
- package/src/components/crm/auth/ClientOTPVerify.jsx +257 -0
- package/src/components/crm/auth/styles/ClientAuth.module.scss +167 -0
- package/src/components/crm/client/ClientDashboard.jsx +62 -0
- package/src/components/crm/client/ClientPortal.jsx +158 -0
- package/src/components/crm/client/styles/ClientDashboard.module.scss +133 -0
- package/src/components/crm/client/styles/ClientPortal.module.scss +240 -0
- package/src/components/crm/generic/GenericAuth.jsx +175 -0
- package/src/components/crm/generic/GenericClientPortal.jsx +165 -0
- package/src/components/crm/generic/GenericReport.jsx +787 -275
- package/src/components/crm/generic/styles/GenericClientPortal.module.scss +154 -0
- package/src/components/crm/generic/styles/GenericReport.module.scss +200 -0
- package/src/index.js +10 -0
|
@@ -14,10 +14,13 @@ import 'react-toastify/dist/ReactToastify.css';
|
|
|
14
14
|
|
|
15
15
|
import CustomFetch from '../Fetch';
|
|
16
16
|
import GenericMain from './GenericMain';
|
|
17
|
+
import GenericClientPortal from './GenericClientPortal';
|
|
17
18
|
import Login from '../auth/Login';
|
|
18
19
|
import Reset from '../auth/Reset';
|
|
19
20
|
import Verify from '../auth/Verify';
|
|
20
21
|
import TwoFactorAuth from '../auth/TwoFactorAuth';
|
|
22
|
+
import ClientLogin from '../auth/ClientLogin';
|
|
23
|
+
import ClientOTPVerify from '../auth/ClientOTPVerify';
|
|
21
24
|
|
|
22
25
|
const ProtectedRoute = ({ user, loading, redirectPath = '/login' }) => {
|
|
23
26
|
const location = useLocation();
|
|
@@ -33,6 +36,24 @@ const ProtectedRoute = ({ user, loading, redirectPath = '/login' }) => {
|
|
|
33
36
|
);
|
|
34
37
|
};
|
|
35
38
|
|
|
39
|
+
const ClientProtectedRoute = ({
|
|
40
|
+
clientUser,
|
|
41
|
+
loading,
|
|
42
|
+
redirectPath = '/client/login',
|
|
43
|
+
}) => {
|
|
44
|
+
const location = useLocation();
|
|
45
|
+
|
|
46
|
+
return loading ? null : !clientUser || !clientUser.id ? (
|
|
47
|
+
<Navigate
|
|
48
|
+
to={redirectPath}
|
|
49
|
+
replace
|
|
50
|
+
state={{ previousUrl: location.pathname }}
|
|
51
|
+
/>
|
|
52
|
+
) : (
|
|
53
|
+
<Outlet />
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
36
57
|
const GenericAuth = ({
|
|
37
58
|
layout = 'full',
|
|
38
59
|
loginBg,
|
|
@@ -43,12 +64,16 @@ const GenericAuth = ({
|
|
|
43
64
|
routeConfig,
|
|
44
65
|
themeType = 'primary',
|
|
45
66
|
enforce2FA = false, // Default to not enforcing 2FA
|
|
67
|
+
clientPortalConfig = null, // Configuration for client portal with component and URLs
|
|
46
68
|
}) => {
|
|
47
69
|
const navigate = useNavigate();
|
|
48
70
|
const location = useLocation();
|
|
49
71
|
const [userProfile, setUserProfile] = useState({});
|
|
72
|
+
const [clientProfile, setClientProfile] = useState({});
|
|
50
73
|
const [isLoading, setIsLoading] = useState(true);
|
|
74
|
+
const [isClientLoading, setIsClientLoading] = useState(true);
|
|
51
75
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
76
|
+
const [isClientAuthenticated, setIsClientAuthenticated] = useState(false);
|
|
52
77
|
|
|
53
78
|
const updateAuthStatus = async () => {
|
|
54
79
|
try {
|
|
@@ -74,10 +99,68 @@ const GenericAuth = ({
|
|
|
74
99
|
}
|
|
75
100
|
};
|
|
76
101
|
|
|
102
|
+
const updateClientAuthStatus = async () => {
|
|
103
|
+
// Skip if client portal config is not provided
|
|
104
|
+
if (!clientPortalConfig) {
|
|
105
|
+
setIsClientLoading(false);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Get the profile URL from config or use default
|
|
110
|
+
const profileUrl =
|
|
111
|
+
clientPortalConfig.urls?.profile || '/client/profile';
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const result = await CustomFetch(profileUrl, 'GET');
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
result.data &&
|
|
118
|
+
result.data[clientPortalConfig.model] &&
|
|
119
|
+
result.data[clientPortalConfig.model].id
|
|
120
|
+
) {
|
|
121
|
+
setClientProfile(result.data[clientPortalConfig.model]);
|
|
122
|
+
setIsClientAuthenticated(true);
|
|
123
|
+
} else {
|
|
124
|
+
setIsClientAuthenticated(false);
|
|
125
|
+
}
|
|
126
|
+
setIsClientLoading(false);
|
|
127
|
+
|
|
128
|
+
// Get login and verify URLs from config
|
|
129
|
+
const loginPath = '/client/login';
|
|
130
|
+
const verifyPath = '/client/verify';
|
|
131
|
+
|
|
132
|
+
// If on client login page but already authenticated, redirect to portal
|
|
133
|
+
if (
|
|
134
|
+
isClientAuthenticated &&
|
|
135
|
+
(location.pathname === loginPath ||
|
|
136
|
+
location.pathname === verifyPath)
|
|
137
|
+
) {
|
|
138
|
+
navigate('/client/portal', { replace: true });
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
setIsClientLoading(false);
|
|
142
|
+
setIsClientAuthenticated(false);
|
|
143
|
+
|
|
144
|
+
// Get login path
|
|
145
|
+
const loginPath = '/client/login';
|
|
146
|
+
|
|
147
|
+
// Only redirect if currently on a client portal page
|
|
148
|
+
if (location.pathname.startsWith('/client/portal')) {
|
|
149
|
+
navigate(loginPath, {
|
|
150
|
+
state: { previousUrl: location.pathname },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
77
156
|
useEffect(() => {
|
|
78
157
|
updateAuthStatus();
|
|
79
158
|
}, [isAuthenticated]);
|
|
80
159
|
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
updateClientAuthStatus();
|
|
162
|
+
}, [isClientAuthenticated, clientPortalConfig, location.pathname]);
|
|
163
|
+
|
|
81
164
|
return (
|
|
82
165
|
<>
|
|
83
166
|
<Routes>
|
|
@@ -93,6 +176,7 @@ const GenericAuth = ({
|
|
|
93
176
|
/>
|
|
94
177
|
}
|
|
95
178
|
/>
|
|
179
|
+
{/* Client Portal Routes */}
|
|
96
180
|
<Route
|
|
97
181
|
path="reset"
|
|
98
182
|
element={
|
|
@@ -124,6 +208,73 @@ const GenericAuth = ({
|
|
|
124
208
|
/>
|
|
125
209
|
}
|
|
126
210
|
/>
|
|
211
|
+
{/* Client Portal Routes and Protected Routes */}
|
|
212
|
+
{clientPortalConfig && (
|
|
213
|
+
<Route path="client">
|
|
214
|
+
{/* Client Auth Routes */}
|
|
215
|
+
<Route
|
|
216
|
+
path="login"
|
|
217
|
+
element={
|
|
218
|
+
<ClientLoginComponent
|
|
219
|
+
loginBg={loginBg}
|
|
220
|
+
logo={logo}
|
|
221
|
+
setIsAuthenticated={
|
|
222
|
+
setIsClientAuthenticated
|
|
223
|
+
}
|
|
224
|
+
setUserProfile={setClientProfile}
|
|
225
|
+
urls={clientPortalConfig.urls}
|
|
226
|
+
/>
|
|
227
|
+
}
|
|
228
|
+
/>
|
|
229
|
+
<Route
|
|
230
|
+
path="verify"
|
|
231
|
+
element={
|
|
232
|
+
<ClientOTPVerifyComponent
|
|
233
|
+
loginBg={loginBg}
|
|
234
|
+
logo={logo}
|
|
235
|
+
setIsAuthenticated={
|
|
236
|
+
setIsClientAuthenticated
|
|
237
|
+
}
|
|
238
|
+
setUserProfile={setClientProfile}
|
|
239
|
+
urls={clientPortalConfig.urls}
|
|
240
|
+
/>
|
|
241
|
+
}
|
|
242
|
+
/>
|
|
243
|
+
|
|
244
|
+
{/* Client Portal Protected Route */}
|
|
245
|
+
{clientPortalConfig.component && (
|
|
246
|
+
<Route
|
|
247
|
+
element={
|
|
248
|
+
<ClientProtectedRoute
|
|
249
|
+
clientUser={clientProfile}
|
|
250
|
+
loading={isClientLoading}
|
|
251
|
+
/>
|
|
252
|
+
}
|
|
253
|
+
>
|
|
254
|
+
<Route
|
|
255
|
+
path="portal/*"
|
|
256
|
+
element={
|
|
257
|
+
<GenericClientPortal
|
|
258
|
+
clientProfile={clientProfile}
|
|
259
|
+
setClientProfile={setClientProfile}
|
|
260
|
+
setClientAuth={
|
|
261
|
+
setIsClientAuthenticated
|
|
262
|
+
}
|
|
263
|
+
logo={headerLogo || logo}
|
|
264
|
+
portalConfig={clientPortalConfig}
|
|
265
|
+
themeType={themeType}
|
|
266
|
+
urls={clientPortalConfig.urls}
|
|
267
|
+
>
|
|
268
|
+
{clientPortalConfig.component}
|
|
269
|
+
</GenericClientPortal>
|
|
270
|
+
}
|
|
271
|
+
/>
|
|
272
|
+
</Route>
|
|
273
|
+
)}
|
|
274
|
+
</Route>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{/* Main Admin Routes */}
|
|
127
278
|
<Route
|
|
128
279
|
element={
|
|
129
280
|
<ProtectedRoute
|
|
@@ -199,4 +350,28 @@ const TwoFactorAuthComponent = React.memo(
|
|
|
199
350
|
)
|
|
200
351
|
);
|
|
201
352
|
|
|
353
|
+
const ClientLoginComponent = React.memo(
|
|
354
|
+
({ loginBg, logo, setIsAuthenticated, setUserProfile, urls }) => (
|
|
355
|
+
<ClientLogin
|
|
356
|
+
loginBg={loginBg}
|
|
357
|
+
logo={logo}
|
|
358
|
+
setSystemAuth={setIsAuthenticated}
|
|
359
|
+
setUserProfile={setUserProfile}
|
|
360
|
+
urls={urls}
|
|
361
|
+
/>
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const ClientOTPVerifyComponent = React.memo(
|
|
366
|
+
({ loginBg, logo, setIsAuthenticated, setUserProfile, urls }) => (
|
|
367
|
+
<ClientOTPVerify
|
|
368
|
+
loginBg={loginBg}
|
|
369
|
+
logo={logo}
|
|
370
|
+
setSystemAuth={setIsAuthenticated}
|
|
371
|
+
setUserProfile={setUserProfile}
|
|
372
|
+
urls={urls}
|
|
373
|
+
/>
|
|
374
|
+
)
|
|
375
|
+
);
|
|
376
|
+
|
|
202
377
|
export default GenericAuth;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Routes, Route, useNavigate } from 'react-router-dom';
|
|
3
|
+
import { toast } from 'react-toastify';
|
|
4
|
+
import { SignOut } from 'akar-icons';
|
|
5
|
+
|
|
6
|
+
import CustomFetch from '../Fetch';
|
|
7
|
+
|
|
8
|
+
import styles from './styles/GenericClientPortal.module.scss';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GenericClientPortal - A wrapper component for client portal content
|
|
12
|
+
*
|
|
13
|
+
* Note: The main component (clientPortalConfig.component) is passed as children
|
|
14
|
+
* from GenericAuth.jsx and rendered in the default route.
|
|
15
|
+
*
|
|
16
|
+
* To pass props to the component, you can provide a function instead of a component instance:
|
|
17
|
+
* clientPortalConfig={{
|
|
18
|
+
* component: (props) => <YourComponent {...props} customProp="value" />,
|
|
19
|
+
* // other config...
|
|
20
|
+
* }}
|
|
21
|
+
*
|
|
22
|
+
* The function will receive { clientProfile, setClientProfile, clientId } as arguments.
|
|
23
|
+
* The clientId is extracted from clientProfile.id for convenience.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} props
|
|
26
|
+
* @param {Object} props.clientProfile - The client user profile data
|
|
27
|
+
* @param {Function} props.setClientProfile - Function to update client profile
|
|
28
|
+
* @param {Function} props.setClientAuth - Function to update client authentication state
|
|
29
|
+
* @param {string} props.logo - URL for the logo to display in header
|
|
30
|
+
* @param {Object} props.portalConfig - Configuration for the portal
|
|
31
|
+
* @param {Object} props.portalConfig.routes - Additional route components to render
|
|
32
|
+
* @param {Object} props.portalConfig.keys - Configuration for mapping client profile fields
|
|
33
|
+
* @param {string|string[]} props.portalConfig.keys.name - Field(s) to use for client name display
|
|
34
|
+
* @param {React.ReactNode|Function} props.portalConfig.component - Main component passed from GenericAuth (received as children prop).
|
|
35
|
+
* If a function is provided, it will be called with { clientProfile, setClientProfile, clientId } as arguments
|
|
36
|
+
* @param {Object} props.urls - URLs for API endpoints
|
|
37
|
+
* @param {string} props.urls.logout - URL for logout endpoint
|
|
38
|
+
* @param {string} props.themeType - Theme type (primary, secondary, etc.)
|
|
39
|
+
* @param {React.ReactNode|Function} props.children - Child components to render in default route.
|
|
40
|
+
* If a function is provided, it will be called with { clientProfile, setClientProfile, clientId } as arguments
|
|
41
|
+
*/
|
|
42
|
+
const GenericClientPortal = ({
|
|
43
|
+
clientProfile,
|
|
44
|
+
setClientProfile,
|
|
45
|
+
setClientAuth,
|
|
46
|
+
logo,
|
|
47
|
+
portalConfig = { routes: {}, keys: {} },
|
|
48
|
+
themeType = 'primary',
|
|
49
|
+
urls = {},
|
|
50
|
+
children,
|
|
51
|
+
}) => {
|
|
52
|
+
const navigate = useNavigate();
|
|
53
|
+
|
|
54
|
+
// Handle logout
|
|
55
|
+
const handleLogout = async () => {
|
|
56
|
+
try {
|
|
57
|
+
const logoutUrl = urls.logout || '/client/logout';
|
|
58
|
+
await CustomFetch(logoutUrl, 'POST', {});
|
|
59
|
+
setClientProfile({});
|
|
60
|
+
setClientAuth(false);
|
|
61
|
+
toast.success('You have been logged out successfully');
|
|
62
|
+
const loginUrl = '/client/login';
|
|
63
|
+
navigate(loginUrl);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
toast.error('Error logging out. Please try again.');
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Get client name based on configuration or fallback to defaults
|
|
70
|
+
const getClientName = () => {
|
|
71
|
+
// Check if keys.name is configured
|
|
72
|
+
if (portalConfig.keys && portalConfig.keys.name) {
|
|
73
|
+
// If keys.name is an array, concatenate the values
|
|
74
|
+
if (Array.isArray(portalConfig.keys.name)) {
|
|
75
|
+
const nameParts = portalConfig.keys.name
|
|
76
|
+
.map((key) => clientProfile[key] || '')
|
|
77
|
+
.filter((part) => part !== '');
|
|
78
|
+
|
|
79
|
+
if (nameParts.length > 0) {
|
|
80
|
+
return nameParts.join(' ');
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// If keys.name is a string, use it directly
|
|
84
|
+
const nameValue = clientProfile[portalConfig.keys.name];
|
|
85
|
+
if (nameValue) return nameValue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fallback to existing fields
|
|
90
|
+
return clientProfile.name || clientProfile.email || 'Client';
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const clientName = getClientName();
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className={`${styles.portalContainer} ${styles[themeType]}`}>
|
|
97
|
+
<header className={styles.portalHeader}>
|
|
98
|
+
<div className={styles.logoContainer}>
|
|
99
|
+
<img
|
|
100
|
+
src={logo}
|
|
101
|
+
alt="Client Portal"
|
|
102
|
+
className={styles.logo}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
<div className={styles.userInfo}>
|
|
106
|
+
<span className={styles.welcomeText}>
|
|
107
|
+
Welcome, <strong>{clientName}</strong>
|
|
108
|
+
</span>
|
|
109
|
+
<button
|
|
110
|
+
onClick={handleLogout}
|
|
111
|
+
className={styles.logoutButton}
|
|
112
|
+
title="Logout"
|
|
113
|
+
aria-label="Logout"
|
|
114
|
+
>
|
|
115
|
+
<SignOut strokeWidth={2} size={16} />
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
</header>
|
|
119
|
+
|
|
120
|
+
<main className={styles.portalContent}>
|
|
121
|
+
<div className={styles.mainContent}>
|
|
122
|
+
<Routes>
|
|
123
|
+
{/* Render provided routes if any */}
|
|
124
|
+
{portalConfig.routes &&
|
|
125
|
+
Object.entries(portalConfig.routes).map(
|
|
126
|
+
([path, Component]) => (
|
|
127
|
+
<Route
|
|
128
|
+
key={path}
|
|
129
|
+
path={path}
|
|
130
|
+
element={
|
|
131
|
+
<Component
|
|
132
|
+
clientProfile={clientProfile}
|
|
133
|
+
setClientProfile={
|
|
134
|
+
setClientProfile
|
|
135
|
+
}
|
|
136
|
+
dataId={
|
|
137
|
+
clientProfile?.id || null
|
|
138
|
+
}
|
|
139
|
+
/>
|
|
140
|
+
}
|
|
141
|
+
/>
|
|
142
|
+
)
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
{/* Default route for main component (passed as children from GenericAuth) */}
|
|
146
|
+
<Route
|
|
147
|
+
path="/"
|
|
148
|
+
element={
|
|
149
|
+
typeof children === 'function'
|
|
150
|
+
? children({
|
|
151
|
+
clientProfile,
|
|
152
|
+
setClientProfile,
|
|
153
|
+
dataId: clientProfile?.id || null,
|
|
154
|
+
})
|
|
155
|
+
: children
|
|
156
|
+
}
|
|
157
|
+
/>
|
|
158
|
+
</Routes>
|
|
159
|
+
</div>
|
|
160
|
+
</main>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export default GenericClientPortal;
|