ui-soxo-bootstrap-core 2.6.14 → 2.6.16

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.
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useContext } from 'react';
1
+ import React, { useState, useEffect, useContext, useRef } from 'react';
2
2
 
3
3
  import { Route, Switch } from 'react-router-dom';
4
4
 
@@ -18,13 +18,33 @@ import PropTypes from 'prop-types';
18
18
 
19
19
  import { MenusAPI, CoreScripts } from '../../models';
20
20
 
21
+ const motivatingMessages = [
22
+ 'Setting things up for a great start...',
23
+ 'Good things are loading. Stay with us.',
24
+ 'Almost there. Preparing your workspace.',
25
+ 'You are one moment away from your dashboard.',
26
+ 'Getting everything ready for you.',
27
+ 'Great care takes a second. Loading now.',
28
+ ];
29
+
30
+ function getRandomMessage(previousMessage = '') {
31
+ const options = motivatingMessages.filter((message) => message !== previousMessage);
32
+
33
+ if (!options.length) {
34
+ return motivatingMessages[0];
35
+ }
36
+
37
+ const randomIndex = Math.floor(Math.random() * options.length);
38
+ return options[randomIndex];
39
+ }
40
+
21
41
  /**
22
42
  * Landing API
23
43
  *
24
44
  * @param {*} param0
25
45
  * @returns
26
46
  */
27
- export default function LandingApi({ history, CustomComponents, CustomModels, appSettings, ...props }) {
47
+ export default function LandingApi({ history, CustomComponents, CustomModels, appSettings, transitionPending = false, onHomeReady, ...props }) {
28
48
  const [loader, setLoader] = useState(false);
29
49
 
30
50
  // const [modules, setModules] = useState([]);
@@ -34,8 +54,11 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
34
54
  const { dispatch, user = {} } = useContext(GlobalContext);
35
55
 
36
56
  const [allModules, setAllModules] = useState();
57
+ const homeReadyNotifiedRef = useRef(false);
58
+ const messageIntervalRef = useRef(null);
37
59
 
38
60
  const [meta, setMeta] = useState({});
61
+ const [loadingMessage, setLoadingMessage] = useState('');
39
62
 
40
63
  // const [reports, setReports] = useState([]);
41
64
 
@@ -70,6 +93,46 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
70
93
  initializeUserMenus();
71
94
  }, []);
72
95
 
96
+ useEffect(() => {
97
+ if (!transitionPending) {
98
+ homeReadyNotifiedRef.current = false;
99
+ return;
100
+ }
101
+
102
+ if (!loader && allModules && !homeReadyNotifiedRef.current) {
103
+ homeReadyNotifiedRef.current = true;
104
+ if (typeof onHomeReady === 'function') {
105
+ onHomeReady();
106
+ }
107
+ }
108
+ }, [transitionPending, loader, allModules, onHomeReady]);
109
+
110
+ useEffect(() => {
111
+ if (!loader) {
112
+ setLoadingMessage('');
113
+
114
+ if (messageIntervalRef.current) {
115
+ clearInterval(messageIntervalRef.current);
116
+ messageIntervalRef.current = null;
117
+ }
118
+
119
+ return;
120
+ }
121
+
122
+ setLoadingMessage((previousMessage) => getRandomMessage(previousMessage));
123
+
124
+ messageIntervalRef.current = setInterval(() => {
125
+ setLoadingMessage((previousMessage) => getRandomMessage(previousMessage));
126
+ }, 2200);
127
+
128
+ return () => {
129
+ if (messageIntervalRef.current) {
130
+ clearInterval(messageIntervalRef.current);
131
+ messageIntervalRef.current = null;
132
+ }
133
+ };
134
+ }, [loader]);
135
+
73
136
  /**
74
137
  * Initialize the user menus
75
138
  */
@@ -245,6 +308,7 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
245
308
  <Card className="skeleton-card">
246
309
  <div className="skeleton-wrapper">
247
310
  <Skeleton paragraph={{ rows: 4 }} />
311
+ <p className="motivating-text">{loadingMessage}</p>
248
312
  </div>
249
313
  </Card>
250
314
  ) : (
@@ -11,9 +11,31 @@
11
11
  }
12
12
 
13
13
  .skeleton-wrapper {
14
+ .motivating-text {
15
+ margin-top: 14px;
16
+ margin-bottom: 4px;
17
+ font-size: 14px;
18
+ line-height: 20px;
19
+ text-align: center;
20
+ color: #5e6d86;
21
+ font-weight: 500;
22
+ animation: skeletonTextFade 0.4s ease-in-out;
23
+ }
14
24
  }
15
25
 
16
26
  .wrapper-loader {
17
27
  margin: 20px;
18
28
  }
19
29
  }
30
+
31
+ @keyframes skeletonTextFade {
32
+ from {
33
+ opacity: 0;
34
+ transform: translateY(2px);
35
+ }
36
+
37
+ to {
38
+ opacity: 1;
39
+ transform: translateY(0);
40
+ }
41
+ }
@@ -16,7 +16,7 @@ import PropTypes from "prop-types";
16
16
  import './phone-input.scss';
17
17
 
18
18
 
19
- export default function CountryPhoneInput({ value, onChange, disabled, disableCountryCode, required, defaultCountryCode, onBlur }) {
19
+ export default function CountryPhoneInput({ value, onChange, disabled, disableCountryCode, required, defaultCountryCode, onBlur, onKeyDown }) {
20
20
 
21
21
  let inputRef = useRef();
22
22
 
@@ -85,7 +85,7 @@ export default function CountryPhoneInput({ value, onChange, disabled, disableCo
85
85
  // country={disableCountryCode ? false : 'in'}
86
86
  value={phone}
87
87
  onChange={handleInput}
88
- inputProps={{ required: required }}
88
+ inputProps={{ required: required, onKeyDown: onKeyDown }}
89
89
  disabled={disabled}
90
90
  onBlur={onBlur}
91
91
  />
@@ -102,7 +102,7 @@ export default function DraggableWrapper({ id, index, movePanel, item, dragEnabl
102
102
  >
103
103
  <div style={{ flex: 1 }}>
104
104
  {dragEnabled && <span style={{ marginRight: 8 }}>⋮⋮</span>}
105
- <span>{item.name}</span>
105
+ <span>{item.caption}</span>
106
106
  {dragEnabled ? (
107
107
  <span style={{ marginLeft: 8, fontSize: 11, color: '#999' }}>(Level {level})</span>
108
108
  ) : (
@@ -79,7 +79,7 @@ export const MenuTree = ({ menus, selectedMenus = [], toggleMenu, parentId = nul
79
79
  {/* Left Side */}
80
80
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
81
81
  {showCheckbox && <Checkbox checked={selectedMenus.includes(menu.id)} onChange={(e) => onParentChange(e.target.checked)} />}
82
- <span>{menu.title || menu.caption}</span>
82
+ <span>{menu.caption}</span>
83
83
  </div>
84
84
  <Tag color={menu.is_visible === true ? 'green' : 'blue'}>{menu.is_visible === true ? 'VISIBLE' : 'HIDDEN'}</Tag>{' '}
85
85
  </div>
@@ -110,7 +110,7 @@ export const MenuTree = ({ menus, selectedMenus = [], toggleMenu, parentId = nul
110
110
  onChange={(e) => onParentChange(e.target.checked)}
111
111
  />
112
112
  )}
113
- <span>{menu.title || menu.caption}</span>
113
+ <span>{menu.caption}</span>
114
114
  </div>
115
115
  <Tag color={menu.is_visible === true ? 'green' : 'blue'}>{menu.is_visible === true ? 'VISIBLE' : 'HIDDEN'}</Tag>{' '}
116
116
  </div>
@@ -4,6 +4,7 @@ Implements utility functions to be used across project
4
4
 
5
5
  /*eslint no-useless-escape:"off",eqeqeq: "off"*/
6
6
  import moment from 'moment';
7
+ import { parsePhoneNumberFromString, isValidPhoneNumber } from 'libphonenumber-js';
7
8
 
8
9
  export function IsObjectHaveKeys(obj) {
9
10
  return obj && typeof obj == 'object' && Object.keys(obj).length;
@@ -221,3 +222,51 @@ export function safeJSON(value) {
221
222
  return null;
222
223
  }
223
224
  }
225
+
226
+ /**
227
+ * Convert +countrycode phone into form compatible structure
228
+ * @param {string} phone
229
+ * @returns {object|null}
230
+ */
231
+ export const formatPhoneForForm = (phone) => {
232
+ if (!phone) return null;
233
+
234
+ const parsed = parsePhoneNumberFromString(phone);
235
+
236
+ if (!parsed) return null;
237
+
238
+ return {
239
+ value: parsed.nationalNumber,
240
+ code: {
241
+ dialCode: parsed.countryCallingCode,
242
+ countryCode: parsed.country?.toLowerCase(),
243
+ },
244
+ valid: true,
245
+ };
246
+ };
247
+
248
+ /**
249
+ * Validates a phone number from CountryPhoneInput.
250
+ * Builds full number using country dial code and checks validity.
251
+ *
252
+ * @param {Object} _ - Ant Design rule parameter (unused)
253
+ * @param {Object} val - Phone value object from form
254
+ * @returns {Promise<void>}
255
+ */
256
+ export const phoneValidator = (_, val) => {
257
+ if (!val || !val.value || !val?.code?.dialCode) {
258
+ return Promise.resolve();
259
+ }
260
+
261
+ const fullPhone = `+${val.code.dialCode}${val.value}`;
262
+
263
+ return isValidPhoneNumber(fullPhone) ? Promise.resolve() : Promise.reject('Invalid mobile number');
264
+ };
265
+
266
+ // Returns phone number with country code (e.g., +919876543210)
267
+ export const formatPhoneWithCountryCode = (phone) => {
268
+ if (phone && typeof phone === 'object' && phone.code) {
269
+ return `+${phone.code.dialCode}${phone.value}`;
270
+ }
271
+ return phone;
272
+ };
@@ -14,7 +14,7 @@ import { GetData, PostData, PutData, DeleteData } from './http/http.utils';
14
14
 
15
15
  import { getExportData } from './generic/generic.utils';
16
16
 
17
- import { ConvertBytesToArray, safeJSON } from './common/common.utils';
17
+ import { ConvertBytesToArray, safeJSON, formatPhoneForForm, phoneValidator, formatPhoneWithCountryCode } from './common/common.utils';
18
18
 
19
19
  import SettingsUtil from './setting.utils';
20
20
 
@@ -37,4 +37,7 @@ export {
37
37
  SettingsUtil,
38
38
  FormUtils,
39
39
  safeJSON,
40
+ formatPhoneForForm,
41
+ phoneValidator,
42
+ formatPhoneWithCountryCode,
40
43
  };
@@ -301,7 +301,7 @@ const MenuTree = ({ menus, selectedMenus, toggleMenu, parentId = null }) => {
301
301
  }
302
302
  }}
303
303
  />
304
- <span>{menu.title || menu.caption}</span>
304
+ <span>{menu.caption}</span>
305
305
  </div>
306
306
  <Tag color={menu.is_visible === true ? 'green' : 'blue'}>{menu.is_visible === true ? 'VISIBLE' : 'HIDDEN'}</Tag>{' '}
307
307
  </div>
@@ -327,7 +327,7 @@ const MenuTree = ({ menus, selectedMenus, toggleMenu, parentId = null }) => {
327
327
  onClick={(e) => e.stopPropagation()}
328
328
  >
329
329
  <Checkbox checked={parentChecked} indeterminate={parentIndeterminate} onChange={(e) => onParentChange(e.target.checked)} />
330
- <span>{menu.title || menu.caption}</span>
330
+ <span>{menu.caption}</span>
331
331
  </div>
332
332
  <Tag color={menu.is_visible === true ? 'green' : 'blue'}>{menu.is_visible === true ? 'VISIBLE' : 'HIDDEN'}</Tag>{' '}
333
333
  </div>
@@ -1,8 +1,9 @@
1
1
  import React, { useState, useEffect, useRef } from 'react';
2
2
 
3
3
  import { Modal, Tabs, Input, Form, Row, Col, message, Checkbox, Select } from 'antd';
4
- import { useTranslation, Button } from './../../../../lib/';
4
+ import { useTranslation, Button, CountryPhoneInput, phoneValidator as libPhoneValidator, formatPhoneForForm } from './../../../../lib/';
5
5
  import { UsersAPI } from '../../..';
6
+ import { formatPhoneWithCountryCode } from '../../../../lib/utils/common/common.utils';
6
7
 
7
8
  const StaffAdd = ({ visible, onCancel, staffId, staffData, onSuccess }) => {
8
9
  const [form] = Form.useForm();
@@ -61,18 +62,6 @@ const StaffAdd = ({ visible, onCancel, staffId, staffData, onSuccess }) => {
61
62
  }
62
63
  }, [visible]);
63
64
 
64
- const phoneValidator = (_, value) => {
65
- if (!value) {
66
- return Promise.resolve(); // not required
67
- }
68
-
69
- if (!/^\d{10}$/.test(value)) {
70
- return Promise.reject('Phone number must be 10 digits');
71
- }
72
-
73
- return Promise.resolve();
74
- };
75
-
76
65
  /** -------------------------------
77
66
  * API CALL – Designations
78
67
  * ------------------------------- */
@@ -201,8 +190,8 @@ const StaffAdd = ({ visible, onCancel, staffId, staffData, onSuccess }) => {
201
190
  shortName: data.shortName,
202
191
  description: data.description,
203
192
  designation: data.designationPtr,
204
- phone1: data.phone || data.mobile,
205
- phone2: data.alternateMobile,
193
+ phone1: formatPhoneForForm(data.phone || data.mobile),
194
+ phone2: formatPhoneForForm(data.alternateMobile),
206
195
  email1: data.email,
207
196
  email2: data.alternateEmail,
208
197
  slno: data.slNo,
@@ -223,14 +212,17 @@ const StaffAdd = ({ visible, onCancel, staffId, staffData, onSuccess }) => {
223
212
  const handleFinish = async (values) => {
224
213
  setLoading(true);
225
214
 
215
+ let phone1Value = formatPhoneWithCountryCode(values.phone1);
216
+ let phone2Value = formatPhoneWithCountryCode(values.phone2);
217
+
226
218
  const payload = {
227
219
  id: values.id,
228
220
  shortName: values.shortName,
229
221
  description: values.description,
230
222
  designationPtr: values.designation ?? existing.designationPtr ?? null,
231
- phone: values.phone1 ?? existing.phone ?? null,
232
- mobile: values.phone1 ?? existing.mobile ?? null,
233
- alternateMobile: values.phone2 ?? existing.alternateMobile ?? null,
223
+ phone: phone1Value ?? existing.phone ?? null,
224
+ mobile: phone1Value ?? existing.mobile ?? null,
225
+ alternateMobile: phone2Value ?? existing.alternateMobile ?? null,
234
226
  email: values.email1 ?? existing.email ?? null,
235
227
  alternateEmail: values.email2 ?? existing.alternateEmail ?? null,
236
228
  remarks: values.remarks ?? existing.remarks ?? null,
@@ -331,26 +323,22 @@ const StaffAdd = ({ visible, onCancel, staffId, staffData, onSuccess }) => {
331
323
  </Form.Item>
332
324
  </Col> */}
333
325
  <Col span={6}>
334
- <Form.Item label="Phone Number" name="phone1" rules={[{ validator: phoneValidator }]}>
335
- <Input
336
- maxLength={10}
337
- autoComplete="off"
338
- placeholder="Enter Phone Number"
339
- inputMode="numeric"
340
- onChange={(e) => (e.target.value = e.target.value.replace(/\D/g, ''))}
326
+ <Form.Item label="Phone Number" name="phone1" validateTrigger="onBlur" rules={[{ validator: libPhoneValidator }]}>
327
+ <CountryPhoneInput
328
+ defaultCountryCode={process.env.REACT_APP_COUNTRYCODE}
329
+ enableSearch
330
+ inputStyle={{ width: '100%' }}
341
331
  onKeyDown={handleEnterKey}
342
332
  />
343
333
  </Form.Item>
344
334
  </Col>
345
335
 
346
336
  <Col span={6}>
347
- <Form.Item label="Alternate Phone Number" name="phone2" rules={[{ validator: phoneValidator }]}>
348
- <Input
349
- maxLength={10}
350
- placeholder="Enter Phone Number"
351
- autoComplete="off"
352
- inputMode="numeric"
353
- onChange={(e) => (e.target.value = e.target.value.replace(/\D/g, ''))}
337
+ <Form.Item label="Alternate Phone Number" name="phone2" validateTrigger="onBlur" rules={[{ validator: libPhoneValidator }]}>
338
+ <CountryPhoneInput
339
+ defaultCountryCode={process.env.REACT_APP_COUNTRYCODE}
340
+ enableSearch
341
+ inputStyle={{ width: '100%' }}
354
342
  onKeyDown={handleEnterKey}
355
343
  />
356
344
  </Form.Item>
@@ -6,7 +6,7 @@ import { Skeleton, Typography, message, Switch, Form, Input, Select, Checkbox, R
6
6
 
7
7
  import AsyncSelect from 'react-select/async';
8
8
 
9
- import { Table, Card, Button, JSONInput, GlobalContext, safeJSON } from './../../../../lib/';
9
+ import { Table, Card, Button, JSONInput, GlobalContext, safeJSON, CountryPhoneInput, phoneValidator } from './../../../../lib/';
10
10
 
11
11
  import { ModelsAPI, PagesAPI, RolesAPI } from '../../..';
12
12
 
@@ -357,6 +357,10 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
357
357
  * Submit values
358
358
  */
359
359
  const onSubmit = (values) => {
360
+ const mobileData = values.mobile;
361
+
362
+ const mobileWithCountryCode = `+${mobileData.code.dialCode}${mobileData.value}`;
363
+
360
364
  values.defaultBranch = String(values.defaultBranch);
361
365
 
362
366
  /**If PanelOpen is open and edit mode then password will be existing password else new password*/
@@ -378,6 +382,7 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
378
382
  values = {
379
383
  ...values,
380
384
  auth_type: 'LDAP',
385
+ mobile: mobileWithCountryCode,
381
386
  FA: authentication,
382
387
  };
383
388
 
@@ -597,23 +602,10 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
597
602
  <Form.Item
598
603
  name="mobile"
599
604
  label="Mobile"
600
- rules={[
601
- { required: true, message: 'Please enter your mobile number' },
602
- {
603
- pattern: /^[0-9]{10}$/,
604
- message: 'Mobile number must be exactly 10 digits',
605
- },
606
- ]}
605
+ validateTrigger="onBlur"
606
+ rules={[{ required: true, message: 'Mobile number required' }, { validator: phoneValidator }]}
607
607
  >
608
- <Input
609
- placeholder="Enter Mobile"
610
- maxLength={10}
611
- onKeyPress={(e) => {
612
- if (!/[0-9]/.test(e.key)) {
613
- e.preventDefault();
614
- }
615
- }}
616
- />
608
+ <CountryPhoneInput defaultCountryCode={process.env.REACT_APP_COUNTRYCODE} enableSearch inputStyle={{ width: '100%' }} />
617
609
  </Form.Item>
618
610
  </Col>
619
611
  <Col span={8}>
@@ -3,6 +3,7 @@ import { Modal, Button, Skeleton } from 'antd';
3
3
  import { EditOutlined } from '@ant-design/icons';
4
4
  import UserAdd from '../../../../models/users/components/user-add/user-add';
5
5
  import { UsersAPI } from '../../..';
6
+ import { formatPhoneForForm } from '../../../../lib';
6
7
 
7
8
  export default function UserEdit(record) {
8
9
  const [visible, setVisible] = useState(false);
@@ -44,13 +45,15 @@ export default function UserEdit(record) {
44
45
  // extract dbPtr (branch code)
45
46
  const defaultBranchCode = defaultBranchObj?.branch_id ? Number(defaultBranchObj.branch_id) : undefined;
46
47
 
48
+ const formattedMobile = formatPhoneForForm(apiData.mobile);
49
+
47
50
  // Construct mapped data object
48
51
  const mappedData = {
49
52
  id: apiData.id,
50
53
  user_type: otherDetails.user_type || apiData.user_type,
51
54
  name: apiData.name,
52
55
  email: apiData.email,
53
- mobile: apiData.mobile,
56
+ mobile: formattedMobile,
54
57
  designation: apiData.designation_code,
55
58
  department: Number(apiData.department_id),
56
59
  // Handle selected branches and default branch
@@ -63,7 +66,7 @@ export default function UserEdit(record) {
63
66
  // Handle doctor codes
64
67
  default_code: apiData.doctor_code,
65
68
  doctor_code: apiData.doctor_code,
66
- staff_code:apiData.staff_id,
69
+ staff_code: apiData.staff_id,
67
70
  auth_type: apiData.auth_type,
68
71
  FA: apiData.FA,
69
72
  active: apiData.active ? true : false,
@@ -13,6 +13,8 @@ export default function ActionButtons({
13
13
  steps,
14
14
  activeStep,
15
15
  isStepCompleted,
16
+ isFullscreen,
17
+ onToggleFullscreen,
16
18
  renderDynamicComponent,
17
19
  handlePrevious,
18
20
  handleNext,
@@ -35,6 +37,10 @@ export default function ActionButtons({
35
37
  Back
36
38
  </Button>
37
39
 
40
+ <Button type="default" onClick={onToggleFullscreen}>
41
+ {isFullscreen ? 'Exit Full Screen' : 'Switch to Full Screen'}
42
+ </Button>
43
+
38
44
  {/* Skip button */}
39
45
  {steps.length > 0 && steps[activeStep]?.allow_skip === 'Y' && (
40
46
  <Button type="default" onClick={handleSkip} disabled={activeStep === steps.length - 1}>
@@ -11,6 +11,7 @@
11
11
  overflow-y: auto;
12
12
  overflow-x: hidden;
13
13
  overscroll-behavior: contain;
14
+
14
15
  padding-bottom: 8px;
15
16
  }
16
17
 
@@ -27,7 +28,6 @@
27
28
  border-top: 1px solid #f0f0f0;
28
29
  box-shadow: 0 -2px 10px rgba(15, 23, 42, 0.04);
29
30
 
30
-
31
31
  width: 61%;
32
32
  padding: 10px;
33
33
  position: fixed;
@@ -37,9 +37,15 @@
37
37
  display: flex;
38
38
  border-radius: 4px;
39
39
  box-shadow: -1px -4px 10px 2px #f7f7f76e;
40
-
40
+ flex-wrap: wrap;
41
41
 
42
42
  .ant-btn {
43
43
  border-radius: 4px;
44
44
  }
45
- }
45
+ }
46
+
47
+ .process-steps-page.is-fullscreen .action-buttons-container {
48
+ width: calc(100% - 24px);
49
+ left: 12px;
50
+ right: 12px;
51
+ }
@@ -8,7 +8,7 @@
8
8
  * - Handles process submission and optional chaining to the next process.
9
9
  * - Provides a collapsible timeline view and action controls.
10
10
  */
11
- import React, { useEffect, useState } from 'react';
11
+ import React, { useEffect, useRef, useState } from 'react';
12
12
  import { Row, Col, Empty } from 'antd';
13
13
  import { Card } from './../../lib';
14
14
  import * as genericComponents from './../../lib';
@@ -36,8 +36,11 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
36
36
  const [timelineCollapsed, setTimelineCollapsed] = useState(false);
37
37
  const [showExternalWindow, setShowExternalWindow] = useState(false);
38
38
  const [externalWin, setExternalWin] = useState(null);
39
+ const [isFullscreen, setIsFullscreen] = useState(false);
40
+ const processStepsRef = useRef(null);
39
41
 
40
42
  const urlParams = Location.search();
43
+ const isConsultationMode = String(urlParams?.consultation).toLowerCase() === 'true';
41
44
  let processId = urlParams.processId;
42
45
  const [currentProcessId, setCurrentProcessId] = useState(processId);
43
46
  // Load process details based on the current process ID
@@ -271,13 +274,49 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
271
274
  };
272
275
  }, [activeStep, steps, externalWin]);
273
276
 
277
+ useEffect(() => {
278
+ const syncFullscreenState = () => {
279
+ setIsFullscreen(document.fullscreenElement === processStepsRef.current);
280
+ };
281
+
282
+ document.addEventListener('fullscreenchange', syncFullscreenState);
283
+ syncFullscreenState();
284
+
285
+ return () => {
286
+ document.removeEventListener('fullscreenchange', syncFullscreenState);
287
+ };
288
+ }, []);
289
+
290
+ const handleToggleFullscreen = async () => {
291
+ const element = processStepsRef.current;
292
+ if (!element) return;
293
+
294
+ try {
295
+ if (document.fullscreenElement === element) {
296
+ if (document.exitFullscreen) {
297
+ await document.exitFullscreen();
298
+ }
299
+ return;
300
+ }
301
+
302
+ if (element.requestFullscreen) {
303
+ await element.requestFullscreen();
304
+ }
305
+ } catch (error) {
306
+ console.error('Unable to toggle full screen for steps page:', error);
307
+ }
308
+ };
309
+
274
310
  /**
275
311
  * Renders the main process UI including timeline, step details,
276
312
  * and action buttons. This content is reused in both normal view
277
313
  * and external window view.
278
314
  */
279
- const renderContent = () => (
280
- <div className="process-steps-page">
315
+ const renderContent = (attachRef = true) => (
316
+ <div
317
+ ref={attachRef ? processStepsRef : null}
318
+ className={`process-steps-page ${isConsultationMode ? 'consultation-mode' : ''} ${isFullscreen ? 'is-fullscreen' : ''}`}
319
+ >
281
320
  <Card className="process-steps-card">
282
321
  <Row gutter={20} className="process-steps-row" align="stretch">
283
322
  <Col xs={24} sm={24} lg={timelineCollapsed ? 2 : 6} className="process-steps-timeline-col">
@@ -302,6 +341,8 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
302
341
  steps={steps}
303
342
  activeStep={activeStep}
304
343
  isStepCompleted={isStepCompleted}
344
+ isFullscreen={isFullscreen}
345
+ onToggleFullscreen={handleToggleFullscreen}
305
346
  renderDynamicComponent={DynamicComponent}
306
347
  handlePrevious={handlePrevious}
307
348
  handleNext={handleNext}
@@ -335,9 +376,9 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
335
376
  width={props.ExternalWindowWidth || 1000}
336
377
  height={props.ExternalWindowHeight || 1000}
337
378
  >
338
- {renderContent()}
379
+ {renderContent(false)}
339
380
  </ExternalWindow>
340
- {renderContent()}
381
+ {renderContent(true)}
341
382
  </>
342
383
  );
343
384
  }
@@ -3,6 +3,25 @@
3
3
  display: flex;
4
4
  flex-direction: column;
5
5
  overflow: hidden;
6
+
7
+ &.consultation-mode {
8
+ background: linear-gradient(180deg, #c8d2d6 0%, #d5d7d1 21%, #d9d8c6 40%, #c6cebe 58%, #a8c0c0 71%, #6e9fbb 84%, #367495 93%, #0a4d6e 100%);
9
+ padding: 12px;
10
+ }
11
+
12
+ &.is-fullscreen {
13
+ padding: 12px;
14
+ }
15
+ }
16
+
17
+ .process-steps-page.consultation-mode .process-steps-card {
18
+ border: none;
19
+ background: transparent;
20
+ box-shadow: none;
21
+ }
22
+
23
+ .process-steps-page.consultation-mode .process-steps-card > .ant-card-body {
24
+ padding: 10px;
6
25
  }
7
26
 
8
27
  .process-steps-card {
@@ -49,11 +68,11 @@
49
68
  flex: 1;
50
69
  max-height: 100%;
51
70
  overflow: hidden;
52
- background: #fff;
53
- border: 1px solid #e5ebf3;
54
- border-radius: 14px;
55
- box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
56
- padding: 14px 16px;
71
+ // background: #fff;
72
+ // border: 1px solid #e5ebf3;
73
+ // border-radius: 14px;
74
+ // box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
75
+ // padding: 14px 16px;
57
76
  position: relative;
58
77
  z-index: 2;
59
78
  }
@@ -84,8 +103,18 @@
84
103
  height: 100%;
85
104
  border: none !important;
86
105
  border-radius: 12px;
87
- background: linear-gradient(180deg, #f4f7fc 0%, #edf2f9 100%);
88
- box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.8), inset 0 8px 16px rgba(15, 23, 42, 0.08), inset 0 -2px 8px rgba(15, 23, 42, 0.04);
106
+ background: linear-gradient(135deg, color-mix(in srgb, var(--accent-color) 15%, var(--surface-color)), var(--surface-color));
107
+
108
+ border: 1px solid color-mix(in srgb, var(--accent-color) 30%, transparent);
109
+
110
+ box-shadow: 0 8px 20px color-mix(in srgb, var(--accent-color) 20%, transparent);
111
+
112
+ transition: all 0.3s ease;
113
+ // background: linear-gradient(180deg, #f4f7fc 0%, #edf2f9 100%);
114
+ // box-shadow:
115
+ // inset 0 1px 2px rgba(255, 255, 255, 0.8),
116
+ // inset 0 8px 16px rgba(15, 23, 42, 0.08),
117
+ // inset 0 -2px 8px rgba(15, 23, 42, 0.04);
89
118
  }
90
119
 
91
120
  .timeline-card .ant-card-body {
@@ -108,6 +137,35 @@
108
137
  transition: all 0.3s ease;
109
138
  height: 100%;
110
139
  overflow-y: auto;
140
+
141
+ scrollbar-width: thin; /* Firefox */
142
+ scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
143
+
144
+ &::-webkit-scrollbar-thumb {
145
+ background: rgba(0, 0, 0, 0);
146
+ }
147
+
148
+ &:hover::-webkit-scrollbar-thumb {
149
+ background: rgba(0, 0, 0, 0.25);
150
+ }
151
+
152
+ &::-webkit-scrollbar {
153
+ width: 6px;
154
+ height: 6px;
155
+ }
156
+
157
+ &::-webkit-scrollbar-track {
158
+ background: transparent;
159
+ }
160
+
161
+ &::-webkit-scrollbar-thumb {
162
+ background: rgba(0, 0, 0, 0.25);
163
+ border-radius: 10px;
164
+ }
165
+
166
+ &::-webkit-scrollbar-thumb:hover {
167
+ background: rgba(0, 0, 0, 0.4);
168
+ }
111
169
  padding-right: 4px;
112
170
  }
113
171
 
@@ -119,7 +177,10 @@
119
177
  border-radius: 10px;
120
178
  background: #fff;
121
179
  padding: 10px 12px;
122
- transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
180
+ transition:
181
+ border-color 0.2s ease,
182
+ box-shadow 0.2s ease,
183
+ transform 0.2s ease;
123
184
  }
124
185
 
125
186
  .timeline-step:hover {
@@ -233,7 +294,11 @@
233
294
  color: #1f3f74;
234
295
  box-shadow: 0 6px 14px rgba(15, 23, 42, 0.2);
235
296
  z-index: 40;
236
- transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
297
+ transition:
298
+ background-color 0.2s ease,
299
+ border-color 0.2s ease,
300
+ box-shadow 0.2s ease,
301
+ transform 0.2s ease;
237
302
 
238
303
  &:hover {
239
304
  background: #f0f6ff;
@@ -265,9 +330,8 @@
265
330
  MOBILE & TABLET VIEW FIXES
266
331
  ============================ */
267
332
 
268
-
269
-
270
- @media (max-width: 992px) { // iPad & tablets
333
+ @media (max-width: 992px) {
334
+ // iPad & tablets
271
335
  .process-steps-page,
272
336
  .process-steps-row {
273
337
  min-height: auto;
@@ -275,6 +339,10 @@
275
339
  overflow: visible;
276
340
  }
277
341
 
342
+ .process-steps-page.consultation-mode {
343
+ padding: 8px;
344
+ }
345
+
278
346
  .process-steps-card,
279
347
  .process-steps-card .ant-card-body,
280
348
  .process-steps-content-col {
@@ -327,7 +395,8 @@
327
395
  }
328
396
  }
329
397
 
330
- @media (max-width: 768px) { // mobile screens
398
+ @media (max-width: 768px) {
399
+ // mobile screens
331
400
  .timeline-sidebar {
332
401
  gap: 8px;
333
402
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-soxo-bootstrap-core",
3
- "version": "2.6.14",
3
+ "version": "2.6.16",
4
4
  "description": "All the Core Components for you to start",
5
5
  "keywords": [
6
6
  "all in one"
@@ -48,6 +48,7 @@
48
48
  "i18next": "^22.4.15",
49
49
  "i18next-browser-languagedetector": "^7.0.1",
50
50
  "jquery": "^3.6.0",
51
+ "libphonenumber-js": "^1.12.39",
51
52
  "lint-staged": "^12.3.2",
52
53
  "moment-timezone": "^0.5.34",
53
54
  "otp-input-react": "^0.3.0",