ui-soxo-bootstrap-core 2.6.1-dev.1 → 2.6.1-dev.2

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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * CommunicationModeSelection Component
3
+ *
4
+ * Renders radio options for selecting OTP delivery method.
5
+ * Supports Email and SMS modes.
6
+ *
7
+ * Props:
8
+ * @param {string} communicationMode - Currently selected mode ('email' | 'mobile')
9
+ * @param {Function} setCommunicationMode - Updates selected mode
10
+ * @param {boolean} modeError - Displays validation error if true
11
+ */
12
+
13
+ import React from 'react';
14
+ import { Radio, Divider, Typography } from 'antd';
15
+ import { MailOutlined, MessageOutlined } from '@ant-design/icons';
16
+
17
+ import './communication-mode-selection.scss';
18
+
19
+ const { Text } = Typography;
20
+
21
+ function CommunicationModeSelection({ communicationMode, setCommunicationMode, modeError }) {
22
+ return (
23
+ <>
24
+ <div className="otp-method-section">
25
+ <Text type="primary">Select Preferred OTP Verification Method</Text>
26
+ <div className="otp-method-group">
27
+
28
+ {/* Email Option */}
29
+ <Radio checked={communicationMode === 'email'} onChange={() => setCommunicationMode('email')}>
30
+ Email <MailOutlined className="otp-icon" style={{ marginLeft: 6 }} />
31
+ </Radio>
32
+
33
+ {/* SMS Option */}
34
+ <Radio checked={communicationMode === 'mobile'} onChange={() => setCommunicationMode('mobile')}>
35
+ SMS <MessageOutlined className="otp-icon" style={{ marginLeft: 6 }} />
36
+ </Radio>
37
+ </div>
38
+
39
+ {/* Validation Error */}
40
+ {modeError && <p className="otp-mode-error">Please select a communication mode.</p>}
41
+ </div>
42
+ </>
43
+ );
44
+ }
45
+
46
+ export default CommunicationModeSelection;
@@ -0,0 +1,60 @@
1
+ $error-color: red;
2
+
3
+ .otp-method-section {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 6px;
7
+
8
+ .otp-method-title {
9
+ font-size: 16px;
10
+ font-weight: 600;
11
+ margin-bottom: 8px;
12
+ }
13
+
14
+ .otp-method-group {
15
+ display: flex;
16
+ align-items: center;
17
+ margin-bottom: 10px;
18
+ gap: 30px;
19
+ font-size: 12px;
20
+
21
+ .ant-radio-wrapper {
22
+ display: flex;
23
+ gap: 6px;
24
+
25
+ svg {
26
+ font-size: 18px;
27
+ opacity: 0.8;
28
+ }
29
+ }
30
+
31
+ @media only screen and (max-width: 600px) {
32
+ flex-direction: column !important;
33
+ align-items: flex-start !important;
34
+ gap: 12px !important;
35
+ }
36
+
37
+ @media only screen and (min-width: 601px) and (max-width: 1024px) {
38
+ flex-direction: row !important;
39
+ align-items: center !important;
40
+ justify-content: space-between !important;
41
+ gap: 20px !important;
42
+ width: 100%;
43
+
44
+ .ant-radio-wrapper {
45
+ flex: 1;
46
+ }
47
+ }
48
+ }
49
+
50
+ .otp-mode-error {
51
+ margin: 5px;
52
+ font-size: 13px;
53
+ color: $error-color;
54
+ }
55
+
56
+ .otp-icon {
57
+ position: relative;
58
+ top: 2px;
59
+ }
60
+ }
@@ -30,10 +30,12 @@ import { getAccessToken, getRefreshToken } from '../../utils/http/auth.helper';
30
30
 
31
31
  import { Location } from '../../utils';
32
32
 
33
- import { checkLicenseStatus, formatMobile, safeJSON } from '../../utils/common/common.utils';
33
+ import { checkLicenseStatus, formatMobile, safeJSON ,checkExpiryStatus} from '../../utils/common/common.utils';
34
34
 
35
35
  import { MailOutlined, MessageOutlined, WhatsAppOutlined } from '@ant-design/icons';
36
36
 
37
+ import ResetPassword from './reset-password';
38
+
37
39
  const { Text, Title } = Typography;
38
40
 
39
41
  const layout = {
@@ -51,6 +53,8 @@ const LICENSE_EXPIRY = '2026-12-12';
51
53
  const headers = {
52
54
  db_ptr: 'nuraho',
53
55
  };
56
+ //password valdity expire
57
+ const PASSWORD_VALIDITY_DAYS = 90;
54
58
 
55
59
  /**
56
60
  *
@@ -89,11 +93,70 @@ function LoginPhone({ history, appSettings }) {
89
93
  const [communicationMode, setCommunicationMode] = useState(null); // default selected email
90
94
  const [modeError, setModeError] = useState(false);
91
95
 
96
+ //for forgot password show
97
+ const [showResetpassword, setShowResetpassword] = useState(false);
98
+
99
+ //for expired password show
100
+ const [expiredPassword, setExpiredPassword] = useState(false);
101
+
102
+ //for default name select when expire case
103
+ const [defaultUsername, setDefaultUsername] = useState('');
104
+
92
105
  const isAuthenticated = Boolean(getAccessToken());
93
106
  const isRefreshTokenExist = Boolean(getRefreshToken());
94
107
 
95
108
  const path = window.location.pathname;
96
109
 
110
+ /**
111
+ * handlePasswordExpiryCheck
112
+ * --------------------------
113
+ * Validates whether a user's password is expired or nearing expiry.
114
+ *
115
+ * - Parses `last_password_change` from user.other_details.
116
+ * - Calculates expiry using PASSWORD_VALIDITY_DAYS.
117
+ * - Uses `checkExpiryStatus()` to determine status.
118
+ * - Shows Ant Design warning message if expired or within warning period.
119
+ * - Warning message includes navigation to `/change-password`.
120
+ *
121
+ * Requires:
122
+ * - PASSWORD_VALIDITY_DAYS constant
123
+ * - checkExpiryStatus utility
124
+ * - React Router history
125
+ * - antd message component
126
+ */
127
+ const handlePasswordExpiryCheck = (user) => {
128
+ const otherDetails = user?.other_details ? JSON.parse(user.other_details) : null;
129
+
130
+ const lastPasswordChange = otherDetails?.last_password_change;
131
+
132
+ if (lastPasswordChange) {
133
+ const passwordExpiryDate = new Date(lastPasswordChange);
134
+ passwordExpiryDate.setDate(passwordExpiryDate.getDate() + PASSWORD_VALIDITY_DAYS);
135
+
136
+ const passwordStatus = checkExpiryStatus({
137
+ expiryDate: passwordExpiryDate,
138
+ warningDays: 2,
139
+ expiredMessage: 'Your password has expired. Please reset it.',
140
+ warningMessage: (d) => (
141
+ <span>
142
+ Your password will expire in {d} day(s).{' '}
143
+ <a
144
+ onClick={() => {
145
+ history.push('/change-password');
146
+ }}
147
+ >
148
+ Click here to update.
149
+ </a>
150
+ </span>
151
+ ),
152
+ });
153
+
154
+ if (passwordStatus.message) {
155
+ message.warning(passwordStatus.message);
156
+ }
157
+ }
158
+ };
159
+
97
160
  const onFinish = (values) => {
98
161
  setLoading(true);
99
162
 
@@ -131,6 +194,8 @@ function LoginPhone({ history, appSettings }) {
131
194
  if (insider_token) localStorage.insider_token = insider_token;
132
195
 
133
196
  if (result.success) {
197
+ handlePasswordExpiryCheck(user);
198
+
134
199
  //two_factor_authentication variable is present then proceed Two factor authentication
135
200
  if (result.data && result.data.two_factor_authentication) {
136
201
  let data;
@@ -183,7 +248,19 @@ function LoginPhone({ history, appSettings }) {
183
248
  history.push('/');
184
249
  }
185
250
  } else {
186
- message.error(result.message);
251
+ if (result.passwordChange) {
252
+ message.warning(result.message);
253
+
254
+ //time for redirect when expire
255
+ setTimeout(() => {
256
+ setExpiredPassword(true);
257
+ setDefaultUsername(values.email);
258
+
259
+ setShowResetpassword(true);
260
+ }, 1500);
261
+ } else {
262
+ message.warning(result.message);
263
+ }
187
264
  }
188
265
  })
189
266
  .catch((error) => {
@@ -342,6 +419,8 @@ function LoginPhone({ history, appSettings }) {
342
419
  // set user info into local storage
343
420
  localStorage.setItem('userInfo', JSON.stringify(userInfo));
344
421
 
422
+ handlePasswordExpiryCheck(result.user);
423
+
345
424
  setTimeout(() => history.push('/'), 500);
346
425
  } else {
347
426
  // OTP FAILED (wrong OTP)
@@ -560,7 +639,7 @@ function LoginPhone({ history, appSettings }) {
560
639
  : {
561
640
  width: '100%',
562
641
  height: '100vh',
563
- backgroundImage: `url(${backgroundImage}), ${state.theme.colors.loginPageBackground}`,
642
+ background: 'linear-gradient(to bottom, #F7F6E3 0%, #EEF1DE 20%, #D5E4DA 45%, #9DBFC8 75%, #4F89A6 100%)',
564
643
  backgroundPosition: 'center bottom, center',
565
644
  backgroundRepeat: 'no-repeat, no-repeat',
566
645
  backgroundSize: 'cover, cover',
@@ -706,7 +785,7 @@ function LoginPhone({ history, appSettings }) {
706
785
  )}
707
786
 
708
787
  {/* Login Form Section */}
709
- {!otpVerification && !otpVisible && (
788
+ {!otpVerification && !otpVisible && !showResetpassword && (
710
789
  <Form {...layout} layout="vertical" name="basic" onFinish={onFinish} onFinishFailed={onFinishFailed}>
711
790
  <div className="form-title">
712
791
  <h4></h4>
@@ -728,19 +807,42 @@ function LoginPhone({ history, appSettings }) {
728
807
  <Input.Password autoComplete="off" />
729
808
  </Form.Item>
730
809
 
731
- <Form.Item {...tailLayout}>
810
+ <Form.Item {...tailLayout} style={{ marginBottom: '0px' }}>
732
811
  <Button loading={loading} type="primary" htmlType="submit" className="SubmitBtn">
733
812
  &nbsp;&nbsp; Submit
734
813
  </Button>
814
+ <div className="forgot-password" style={{ marginTop: '8px', textAlign: 'center' }}>
815
+ <Link onClick={() => setShowResetpassword(true)}>Forgot Password?</Link>
816
+ </div>
735
817
  </Form.Item>
736
818
  </Form>
737
819
  )}
820
+
821
+ {/* Forgot Password Section */}
822
+ {showResetpassword && (
823
+ <ResetPassword
824
+ defaultUsername={expiredPassword ? defaultUsername : undefined}
825
+ disabledUserName={expiredPassword}
826
+ onBack={() => {
827
+ setShowResetpassword(false);
828
+ setExpiredPassword(false);
829
+ }}
830
+ title={expiredPassword ? 'Password Expired' : 'Forgot Password'}
831
+ subtitle={
832
+ expiredPassword
833
+ ? 'Enter your username and choose your preferred OTP method to receive a reset link.'
834
+ : 'Enter your username to reset your password and select your preferred OTP method.'
835
+ }
836
+ buttonText={expiredPassword ? 'Send Reset Link' : 'Reset Password'}
837
+ />
838
+ )}
738
839
  </div>
739
840
  </div>
740
841
  {!otpSuccess && otpVerification && (
741
842
  <div className="otp-actions">
742
843
  <div className="resend-action">
743
844
  <Text disabled>Didn't receive OTP?</Text>
845
+
744
846
  <Link className="resend-otp-link" disabled={!otpExpired} onClick={handleResendOTP}>
745
847
  Resend OTP
746
848
  </Link>
@@ -1,17 +1,35 @@
1
+ // Variables
2
+ $primary-color: #0c66e4;
3
+ $bg-color-light: #f5f5f5;
4
+ $border-color-light: #e8e8e8;
5
+ $border-color-medium: #e0e0e0;
6
+ $border-color-dark: #d9d9d9;
7
+ $text-color-dark: #071822;
8
+ $white: #ffffff;
9
+ $error-color: red;
10
+
1
11
  body {
2
12
  overflow: hidden;
3
13
  }
4
14
 
15
+ .full-page {
16
+ height: 100vh;
17
+ min-height: 100vh;
18
+ width: 100%;
19
+ background-repeat: no-repeat;
20
+ background-position: bottom center;
21
+ background-size: 100% auto;
22
+ }
23
+
5
24
  .user-authentication-section {
6
25
  min-height: 90vh;
7
26
  display: flex;
8
- // margin: 0px 15%;
9
27
  justify-content: center;
10
- padding: 0px 15%;
11
- // align-items: center;
28
+ padding: 0 15%;
12
29
 
13
30
  @media (max-width: 1200px) {
14
31
  margin: 0 5%;
32
+ padding: 0 5%;
15
33
  }
16
34
 
17
35
  @media (max-width: 768px) {
@@ -21,63 +39,22 @@ body {
21
39
  align-items: center;
22
40
  }
23
41
 
42
+ @media only screen and (min-width: 608px) {
43
+ justify-content: center;
44
+ align-items: center;
45
+ }
46
+
24
47
  .page-background {
25
48
  position: absolute;
26
- left: 0px;
27
- top: 0px;
49
+ left: 0;
50
+ top: 0;
28
51
  height: 40vh;
29
52
  width: 100%;
30
-
31
- // background-image:url("./../../../assets/images/vector.png"); /* Set Image */ ;
32
- // background: url("./../../../assets/images/vector.png"),
33
- // linear-gradient(to right, #D5F1FB, #24AEB8);
34
- // background-size: cover;
35
- // background-position: center;
36
- // background: linear-gradient(to right, #7ddafac2, #24AEB8); /* Gradient */
37
- // url("./../../../assets/images/vector.png"),
38
- // /* Background image */ linear-gradient(to right, #7ddafac2, #10c7ef61); /* Gradient */
39
-
40
- // background: #00b4db; /* fallback for old browsers */
41
- // background: -webkit-linear-gradient(to right, #0083b0, #00b4db); /* Chrome 10-25, Safari 5.1-6 */
42
- // background: linear-gradient(
43
- // to right,
44
- // #0083b0,
45
- // #00b4db
46
- // ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
47
-
48
- // background: rgb(131, 58, 180);
49
- // background: -moz-linear-gradient(
50
- // 90deg,
51
- // rgba(131, 58, 180, 1) 0%,
52
- // rgba(253, 29, 29, 1) 50%,
53
- // rgba(252, 176, 69, 1) 100%
54
- // );
55
- // background: -webkit-linear-gradient(
56
- // 90deg,
57
- // rgba(131, 58, 180, 1) 0%,
58
- // rgba(253, 29, 29, 1) 50%,
59
- // rgba(252, 176, 69, 1) 100%
60
- // );
61
-
62
- // background: linear-gradient(90deg, rgba(131, 58, 180, 1) 0%, rgba(253, 29, 29, 1) 50%, rgba(252, 176, 69, 1) 100%);
63
- // filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#833ab4",endColorstr="#fcb045",GradientType=1);
64
-
65
- // background-image: linear-gradient(to right top, #051937, #384164, #6b6e95, #a09ec9, #d9d1ff);
66
- // background: #BAD7FF;
67
- }
68
-
69
- @media screen and (max-width: 1200px) {
70
- margin: 0px 5%;
71
- }
72
-
73
- @media only screen and (min-width: 608px) {
74
- justify-content: center;
75
- align-items: center;
76
53
  }
77
54
 
78
55
  .homescreen {
79
56
  width: 100%;
80
- margin: 10px 0px;
57
+ margin: 10px 0;
81
58
  border-radius: 4px;
82
59
  background-color: aliceblue;
83
60
  padding: 10px;
@@ -85,222 +62,267 @@ body {
85
62
  overflow: hidden;
86
63
  }
87
64
 
88
- .customers {
65
+ .customers,
66
+ .customers2 {
89
67
  width: 50%;
90
- margin: 20px 0px;
68
+ margin: 20px 0;
91
69
 
92
70
  @media only screen and (min-width: 768px) {
93
71
  width: 50%;
94
- /* margin: 20px 0px; */
95
- /* float: right; */
96
- // position: absolute;
97
- // right: 0px;
98
- // top: 20%;
99
72
  }
73
+ @media only screen and (max-width: 768px) {
74
+ width: 85%;
75
+ }
76
+ }
100
77
 
78
+ .customers {
101
79
  @media only screen and (min-width: 769px) {
102
80
  display: none;
103
81
  }
104
-
105
- @media only screen and (max-width: 768px) {
106
- width: 85%;
107
- }
108
82
  }
109
83
 
110
84
  .customers2 {
111
- width: 50%;
112
- margin: 20px 0px;
113
85
  padding: 0 20px;
114
-
115
86
  @media only screen and (min-width: 768px) {
116
87
  width: 40%;
117
- /* margin: 20px 0px; */
118
- /* float: right; */
119
- // position: absolute;
120
- // right: 0px;
121
- // top: 20%;
122
88
  }
123
-
124
89
  @media only screen and (max-width: 768px) {
125
90
  display: none;
126
91
  }
127
92
  }
128
- .brand-logo {
129
- width: 80px;
130
- box-shadow:
131
- 1px 0 8px 0 rgba(0, 0, 0, 0.05),
132
- 8px 8px 18px 0 rgba(0, 0, 0, 0.05);
133
- border: 1px solid #b7b7b7;
134
- text-align: center;
135
- border-radius: 2px;
136
- margin: 22px 0px;
137
- padding: 4px;
138
-
139
- img {
140
- width: 80px;
141
- }
142
- }
143
-
144
- .footer-logo {
145
- width: 180px;
146
- // box-shadow: 1px 0 8px 0 rgba(0, 0, 0, 0.05), 8px 8px 18px 0 rgba(0, 0, 0, 0.05);
147
- // border: 1px solid #b7b7b7;
148
- padding: 4px;
149
- border-radius: 2px;
150
- margin: 20px 0px 30px;
151
- }
152
93
 
153
94
  .auth-form-wrapper {
154
- // padding: 15px 10px;
155
95
  box-sizing: border-box;
156
- // box-shadow: 0 2px 4px rgba(104, 97, 97, 0.5);
157
- // display: flex;
158
- // align-items: center;
159
96
  border-radius: 16px;
160
- background-color: white;
97
+ background-color: $white;
161
98
  overflow: hidden;
162
- border: 1px solid #e8e8e8;
163
-
164
- width: 42%;
99
+ // border: 1px solid $border-color-light;
100
+ width: 40%;
101
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
165
102
 
166
103
  @media only screen and (max-width: 768px) {
167
104
  width: 100%;
168
105
  padding: 0 16px;
169
-
170
- // display: flex;
171
- // flex-direction: column;
172
- // justify-content: center;
173
- // text-align: center;
174
- // align-items: center;
106
+ flex-direction: column;
175
107
  }
176
108
 
177
109
  .login-form-container {
178
- border: 1px solid #e0e0e0;
110
+ // border: 1px solid $border-color-medium;
179
111
  flex-basis: 50%;
180
- // border: none !important;
181
- // box-shadow: none !important;
182
- // padding: 20px 30px;
183
112
  padding: 20px;
184
- padding-top: 2px;
113
+ // padding-top: 2px;
185
114
 
186
115
  @media only screen and (max-width: 768px) {
187
116
  padding: 16px;
188
- // display: flex;
189
- // flex-direction: column;
190
- // justify-content: center;
191
- // text-align: center;
192
- // align-items: center;
193
117
  }
194
118
 
195
- .branch-switcher {
196
- .branches {
197
- .ant-select {
198
- min-width: auto !important;
199
- width: 100%;
119
+ .branch-switcher .branches .ant-select {
120
+ min-width: auto !important;
121
+ width: 100%;
122
+ }
123
+
124
+ .brand-logo {
125
+ width: 80px;
126
+ box-shadow:
127
+ 1px 0 8px 0 rgba(0, 0, 0, 0.05),
128
+ 8px 8px 18px 0 rgba(0, 0, 0, 0.05);
129
+ border: 1px solid #b7b7b7;
130
+ text-align: center;
131
+ border-radius: 2px;
132
+ // margin: 22px 0;
133
+ padding: 4px;
134
+
135
+ img {
136
+ width: 80px;
137
+ }
138
+ }
139
+
140
+ .otp-form {
141
+ margin-top: 20px;
142
+ .otp-input-container {
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: 2px;
146
+
147
+ p {
148
+ margin: 12px 0 0 0;
149
+ font-size: 12px;
150
+ }
151
+
152
+ .otp-title {
153
+ @media only screen and (max-width: 768px) {
154
+ text-align: left;
155
+ }
156
+ }
157
+
158
+ .otp-method-section {
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: 6px;
162
+
163
+ .otp-method-title {
164
+ font-size: 16px;
165
+ font-weight: 600;
166
+ margin-bottom: 8px;
167
+ }
168
+
169
+ .otp-method-group {
170
+ display: flex;
171
+ align-items: center;
172
+ margin-bottom: 10px;
173
+ gap: 30px;
174
+ font-size: 12px;
175
+
176
+ .ant-radio-wrapper {
177
+ display: flex;
178
+ gap: 6px;
179
+
180
+ svg {
181
+ font-size: 18px;
182
+ opacity: 0.8;
183
+ }
184
+ }
185
+
186
+ @media only screen and (max-width: 600px) {
187
+ flex-direction: column !important;
188
+ align-items: flex-start !important;
189
+ gap: 12px !important;
190
+ }
191
+
192
+ @media only screen and (min-width: 601px) and (max-width: 1024px) {
193
+ flex-direction: row !important;
194
+ align-items: center !important;
195
+ justify-content: space-between !important;
196
+ gap: 20px !important;
197
+ width: 100%;
198
+
199
+ .ant-radio-wrapper {
200
+ flex: 1;
201
+ }
202
+ }
203
+ }
204
+
205
+ .otp-mode-error {
206
+ margin: 5px;
207
+ font-size: 13px;
208
+ color: $error-color;
209
+ }
210
+
211
+ .otp-icon {
212
+ position: relative;
213
+ top: 2px;
214
+ }
215
+ }
216
+
217
+ .otp-container {
218
+ display: flex;
219
+ flex-direction: column;
220
+ gap: 7px;
221
+
222
+ input[type='text']:focus {
223
+ border-color: $white !important;
224
+ }
225
+ }
226
+
227
+ .otp-mode-text {
228
+ text-transform: capitalize;
200
229
  }
201
230
  }
202
231
  }
203
232
  }
204
233
 
205
- .center-line {
206
- border-left: 2px solid #d9d9d9;
207
- height: 180px;
234
+ .otp-actions {
235
+ background-color: $bg-color-light;
236
+ padding: 12px 16px;
237
+ display: flex;
238
+ justify-content: space-between;
239
+ gap: 6px;
240
+
241
+ .resend-action {
242
+ margin-bottom: 8px;
243
+ display: flex;
244
+ gap: 6px;
245
+
246
+ .resend-otp-link {
247
+ display: flex;
248
+ justify-content: center;
249
+ align-items: center;
250
+ text-align: center;
251
+ color: $primary-color;
252
+ margin-bottom: 10px;
253
+ margin: auto;
254
+ }
255
+ }
208
256
  }
209
257
 
210
258
  .center-line {
259
+ border-left: 2px solid $border-color-dark;
260
+ height: 180px;
261
+
211
262
  @media only screen and (max-width: 768px) {
212
263
  display: none;
213
264
  }
214
265
  }
215
-
216
- @media only screen and (max-width: 768px) {
217
- padding: 0;
218
- // min-width: 275px;
219
- flex-direction: column;
220
- }
221
- .form-title {
222
- // h4 {
223
- // color: #071822;
224
- // font-weight: 600;
225
- // // size: 13px;
226
- // margin: 10px 0px;
227
- // font-size: 20px;
228
-
229
- // @media only screen and (max-width: 1400px) {
230
- // line-height: 30px;
231
- // }
232
- }
233
266
  }
267
+
268
+ // Scoped Ant Design Overrides
234
269
  .ant-form {
235
- // height: 100%;
270
+ .ant-form-item:not(:last-child) {
271
+ margin: 20px 0;
272
+ }
236
273
  .ant-form-item {
237
- margin: 20px 0px;
274
+ margin: 20px 0;
275
+
238
276
  .ant-form-item-label {
239
277
  margin: 0 0 8px 0;
240
278
  line-height: 0;
241
279
  padding: 0;
280
+
281
+ > label {
282
+ color: var(--custom-text-color);
283
+ }
242
284
  }
243
285
  }
244
- .ant-btn-primary {
245
- margin-top: 10px;
286
+
287
+ .ant-form-explain {
288
+ position: absolute;
289
+ font-size: 12px;
290
+ margin-left: 2px;
246
291
  }
247
292
  }
248
- .ant-form-explain {
249
- position: absolute;
250
- font-size: 12px;
251
- margin-left: 2px;
293
+
294
+ .ant-form-vertical .ant-form-item .ant-form-item-control {
295
+ min-width: 100%;
252
296
  }
297
+
253
298
  .ant-btn-primary {
254
- // width: 100%;
255
- background-color: #0c66e4;
256
- // height: 35px;
299
+ margin-top: 10px;
300
+ background-color: $primary-color;
257
301
  border-radius: 4px;
258
302
  width: 100%;
259
- @media only screen and (max-width: 768px) {
260
- width: 100%;
261
- background-color: #0c66e4;
262
- // min-width: 275px;
263
- }
264
303
  }
304
+
265
305
  .ant-btn-secondary {
266
306
  width: 100%;
267
307
  }
268
- }
269
- .otp-container {
270
- display: flex;
271
- }
272
308
 
273
- .otp-input-container p {
274
- margin: 12px 0 0 0;
275
- font-size: 12px;
276
- }
277
-
278
- .otp-input-container .otp-title {
279
- @media only screen and (max-width: 768px) {
280
- text-align: left;
309
+ .ant-divider-horizontal {
310
+ margin-bottom: 16px;
311
+ margin-top: 2px;
281
312
  }
282
- }
283
- .ant-divider-horizontal {
284
- margin-bottom: 16px;
285
- margin-top: 2px;
286
- }
287
313
 
288
- .otp-input-container .resend-otp-link {
289
- display: flex;
290
- justify-content: center;
291
- align-items: center;
292
- text-align: center;
293
- // margin-top: -10px;
294
- color: #0c66e4;
295
- margin-bottom: 10px;
296
- }
314
+ .ant-input {
315
+ background-color: transparent !important;
316
+ }
297
317
 
298
- .ant-form-vertical .ant-form-item .ant-form-item-control {
299
- min-width: 100%;
300
- }
318
+ input {
319
+ border: 1px solid $border-color-dark;
320
+ border-radius: 2px;
301
321
 
302
- .resend-otp-link {
303
- margin: auto;
322
+ &:focus {
323
+ border-color: $white !important;
324
+ }
325
+ }
304
326
  }
305
327
 
306
328
  .footer {
@@ -312,147 +334,11 @@ body {
312
334
  @media only screen and (max-width: 768px) {
313
335
  display: none;
314
336
  }
315
- }
316
-
317
- .otp-input-container {
318
- display: flex;
319
- flex-direction: column;
320
- gap: 2px;
321
- }
322
- .otp-actions {
323
- background-color: #f5f5f5; // light gray
324
- padding: 12px 16px;
325
- display: flex;
326
- justify-content: space-between;
327
- gap: 6px;
328
- // margin-top: 6px;
329
- }
330
- .resend-action {
331
- margin-bottom: 8px;
332
- display: flex;
333
- gap: 6px;
334
- }
335
- .otp-method-section {
336
- // margin-top: 10px;
337
-
338
- display: flex;
339
- flex-direction: column;
340
- gap: 6px;
341
-
342
- .otp-method-title {
343
- font-size: 16px;
344
- font-weight: 600;
345
- margin-bottom: 8px;
346
- }
347
-
348
- .otp-method-group {
349
- display: flex;
350
- align-items: center;
351
- margin-bottom: 10px;
352
- gap: 30px;
353
- font-size: 12px;
354
- }
355
- .otp-icon {
356
- position: relative;
357
- top: 2px;
358
- }
359
-
360
- .ant-radio-wrapper {
361
- display: flex;
362
- // align-items: center;
363
- gap: 6px;
364
-
365
- svg {
366
- font-size: 18px;
367
- opacity: 0.8;
368
- }
369
- }
370
-
371
- .otp-mode-error {
372
- margin: 5px;
373
- font-size: 13px;
374
- color: red;
375
- }
376
- }
377
-
378
- .otp-mode-text {
379
- text-transform: capitalize;
380
- }
381
-
382
- input {
383
- border: 1px solid #d9d9d9;
384
- border-radius: 2px;
385
- // padding: 30px;
386
- }
387
-
388
- input:focus {
389
- border-color: #fff !important;
390
- }
391
- .otp-container {
392
- display: flex;
393
- flex-direction: column;
394
- gap: 7px;
395
- }
396
337
 
397
- .otp-container input[type='text']:focus {
398
- border-color: #fff !important;
399
- }
400
-
401
- .full-page {
402
- height: 100vh;
403
- background-color: 'cover'; // Ensures full coverage
404
- background-position: 'bottom center'; // Aligns image at the bottom
405
- background-size: '100% auto'; // image spans full width
406
- background-repeat: no-repeat;
407
-
408
- min-height: 100vh; // Ensures full height
409
- width: 100%; // Covers the full width
410
- }
411
- .ant-input {
412
- background-color: transparent !important;
413
- }
414
-
415
- .ant-form-item-label > label {
416
- color: var(--custom-text-color);
417
- }
418
-
419
- // .user-authentication-section .auth-form-wrapper .ant-btn-primary {
420
- // background-color: var(--custom-bg-color);
421
- // color: var(--custom-btn-text-color);
422
- // }
423
- // .ant-input {
424
- // background-color: var(--custom-input-bg-color);
425
- // color: var(--custom-input-color);
426
- // }
427
- /* Mobile: stack OTP radio options in separate rows */
428
- /* Mobile phones (iPhone) – stack in one column */
429
- @media only screen and (max-width: 600px) {
430
- .otp-method-group {
431
- flex-direction: column !important;
432
- align-items: flex-start !important;
433
- gap: 12px !important;
434
- }
435
- }
436
-
437
- /* iPad Mini + Tablets (600px to 1024px) – show in row */
438
- @media only screen and (min-width: 601px) and (max-width: 1024px) {
439
- .otp-method-group {
440
- flex-direction: row !important;
441
- align-items: center !important;
442
- justify-content: space-between !important;
443
- gap: 20px !important;
444
- width: 100%;
445
- }
446
-
447
- .otp-method-group .ant-radio-wrapper {
448
- flex: 1; /* evenly spaced columns */
449
- }
450
- }
451
-
452
- /* Desktop – normal layout */
453
- @media only screen and (min-width: 1025px) {
454
- .otp-method-group {
455
- flex-direction: row;
456
- gap: 30px;
338
+ .footer-logo {
339
+ width: 180px;
340
+ padding: 4px;
341
+ border-radius: 2px;
342
+ margin: 20px 0 30px;
457
343
  }
458
344
  }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * ResetPassword Component
3
+ *
4
+ * Handles staff password reset flow:
5
+ * - Accepts username
6
+ * - Selects communication mode (email/SMS)
7
+ * - Calls API to send reset link
8
+ * - Displays success/error messages
9
+ * - Allows navigation back to login
10
+ *
11
+ * Props:
12
+ * @param {Function} onBack - Triggered when "Back to Login" is clicked
13
+ * @param {string} title - Page title
14
+ * @param {string} subtitle - Page subtitle
15
+ * @param {string} buttonText - Submit button text
16
+ * @param {string} defaultUsername - Pre-filled username
17
+ * @param {boolean} disabledUserName - Disable username field
18
+ */
19
+
20
+ import React, { useState, useEffect } from 'react';
21
+ import { Divider, Form, Input, message } from 'antd';
22
+ import { Button } from '../../elements';
23
+ import CommunicationModeSelection from './commnication-mode-selection';
24
+ import { motion } from 'framer-motion';
25
+ import './reset-password.scss';
26
+ import { UsersAPI } from '../../../models';
27
+
28
+ function ResetPassword({ onBack, title, subtitle, buttonText, defaultUsername, disabledUserName }) {
29
+ // Selected communication mode (default: email) */
30
+ const [communicationMode, setCommunicationMode] = useState('email');
31
+
32
+ const [modeError, setModeError] = useState(false);
33
+
34
+ // Loading state for submit button */
35
+ const [loading, setLoading] = useState(false);
36
+
37
+ const [form] = Form.useForm();
38
+
39
+ // Pre-fills username if provided and disabled.
40
+ useEffect(() => {
41
+ if (disabledUserName && defaultUsername) {
42
+ form.setFieldsValue({ username: defaultUsername });
43
+ }
44
+ }, [disabledUserName, defaultUsername]);
45
+
46
+ /**
47
+ * Sends forgot password request to backend.
48
+ * Payload: { mode, username, user_type: 'staff' }
49
+ */
50
+ const handleSendForgetPassword = async (values) => {
51
+ const payload = {
52
+ mode: communicationMode,
53
+ username: values.username,
54
+ user_type: 'staff',
55
+ };
56
+ setLoading(true);
57
+
58
+ try {
59
+ const res = await UsersAPI.createForgotePassword(payload);
60
+
61
+ if (!res?.success) {
62
+ throw new Error(res?.message || 'Reset password failed');
63
+ }
64
+
65
+ message.success(res?.message || 'Reset password link sent');
66
+ } catch (err) {
67
+ message.warning(err?.message || 'Reset password failed');
68
+ } finally {
69
+ setLoading(false);
70
+ }
71
+ };
72
+
73
+ /**
74
+ * Clears auth storage and navigates back.
75
+ */
76
+ const handleBackToLogin = () => {
77
+ localStorage.removeItem('access_token');
78
+ localStorage.removeItem('refresh_token');
79
+ sessionStorage.clear();
80
+ onBack();
81
+ };
82
+
83
+ return (
84
+ <motion.div
85
+ className="forgot-password-container"
86
+ initial={{ opacity: 0, y: 70 }}
87
+ animate={{ opacity: 1, y: 0 }}
88
+ exit={{ opacity: 0, y: -50 }}
89
+ transition={{
90
+ y: {
91
+ duration: 1,
92
+ ease: [0.22, 0.08, 0.26, 1],
93
+ },
94
+ opacity: {
95
+ duration: 0.45,
96
+ ease: 'easeOut',
97
+ },
98
+ }}
99
+ >
100
+ <h3 className="password-title">{title}</h3>
101
+ <p className="password-subtitle">{subtitle}</p>
102
+ <Divider />
103
+ <Form layout="vertical" form={form} onFinish={handleSendForgetPassword}>
104
+ <Form.Item label="Username" name="username" rules={[{ required: true, message: 'Please input your username' }]}>
105
+ <Input placeholder="Enter your username" disabled={disabledUserName} />
106
+ </Form.Item>
107
+
108
+ <CommunicationModeSelection communicationMode={communicationMode} setCommunicationMode={setCommunicationMode} modeError={modeError} />
109
+
110
+ <Button type="primary" htmlType="submit" loading={loading}>
111
+ {buttonText}
112
+ </Button>
113
+
114
+ <div style={{ marginTop: '10px', textAlign: 'center' }}>
115
+ <a style={{ cursor: 'pointer' }} onClick={handleBackToLogin}>
116
+ Back to Login
117
+ </a>
118
+ </div>
119
+ </Form>
120
+ </motion.div>
121
+ );
122
+ }
123
+
124
+ export default ResetPassword;
@@ -0,0 +1,22 @@
1
+ .forgot-password-container {
2
+
3
+ padding: 4px;
4
+ .password-title{
5
+ color: #1f1f1f;
6
+ margin: 0 0 8px;
7
+ }
8
+
9
+ .password-subtitle {
10
+ font-size: 14px;
11
+ font-weight: 400;
12
+ color: #6b7280;
13
+ line-height: 1.5;
14
+ margin: 0 0 20px;
15
+ }
16
+
17
+ .ant-divider {
18
+ margin: 0;
19
+ border-top: 1px solid #e5e7eb;
20
+ }
21
+ }
22
+
@@ -122,6 +122,42 @@ export const checkLicenseStatus = (expiryDate) => {
122
122
  return { valid: true, daysLeft, message: null, level: null };
123
123
  };
124
124
 
125
+ /**
126
+ * Checks password expiry status.
127
+ *
128
+ * @param {string|Date} expiryDate
129
+ * @param {number} warningDays
130
+ * @param {string} expiredMessage
131
+ * @param {(daysLeft: number) => string} warningMessage
132
+ *
133
+ * @returns {{ valid: boolean, daysLeft: number, message: string|null, level: "error"|"warning"|null }}
134
+ */
135
+
136
+ export const checkExpiryStatus = ({ expiryDate, warningDays, expiredMessage, warningMessage }) => {
137
+ const expiry = new Date(expiryDate);
138
+
139
+ if (isNaN(expiry)) {
140
+ return { valid: false, daysLeft: 0, message: 'Invalid date', level: 'error' };
141
+ }
142
+
143
+ expiry.setHours(0, 0, 0, 0);
144
+ const today = new Date();
145
+ today.setHours(0, 0, 0, 0);
146
+
147
+ const msDiff = expiry.getTime() - today.getTime();
148
+ const daysLeft = Math.ceil(msDiff / (1000 * 60 * 60 * 24));
149
+
150
+ if (daysLeft < 0) {
151
+ return { valid: false, daysLeft, message: expiredMessage, level: 'error' };
152
+ }
153
+
154
+ if (daysLeft <= warningDays) {
155
+ return { valid: true, daysLeft, message: warningMessage(daysLeft), level: 'warning' };
156
+ }
157
+
158
+ return { valid: true, daysLeft, message: null, level: null };
159
+ };
160
+
125
161
  /**
126
162
  * Masks a mobile number by hiding all but the last `visibleDigits`.
127
163
  * Adds spacing only to the masked portion (groups of 3),
@@ -359,6 +359,13 @@ class Users extends Base {
359
359
  url: 'staff/get-all-staff',
360
360
  });
361
361
  };
362
+
363
+ createForgotePassword = (formBody) => {
364
+ return ApiUtils.post({
365
+ url: `bookings/trigger-reset-password-link`,
366
+ formBody,
367
+ });
368
+ };
362
369
  }
363
370
 
364
371
  export default Users;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-soxo-bootstrap-core",
3
- "version": "2.6.1-dev.1",
3
+ "version": "2.6.1-dev.2",
4
4
  "description": "All the Core Components for you to start",
5
5
  "keywords": [
6
6
  "all in one"