ui-soxo-bootstrap-core 2.4.24 → 2.4.25-dev.11

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.
Files changed (37) hide show
  1. package/.github/workflows/npm-publish.yml +37 -15
  2. package/core/components/extra-info/extra-info-details.js +109 -126
  3. package/core/components/landing-api/landing-api.js +22 -30
  4. package/core/lib/Store.js +20 -18
  5. package/core/lib/components/index.js +4 -1
  6. package/core/lib/components/sidemenu/sidemenu.js +153 -256
  7. package/core/lib/components/sidemenu/sidemenu.scss +39 -26
  8. package/core/lib/elements/basic/dragabble-wrapper/draggable-wrapper.js +119 -42
  9. package/core/lib/elements/basic/rangepicker/rangepicker.js +118 -29
  10. package/core/lib/elements/basic/switch/switch.js +35 -25
  11. package/core/lib/hooks/index.js +2 -12
  12. package/core/lib/hooks/use-otp-timer.js +99 -0
  13. package/core/lib/pages/login/login.js +255 -139
  14. package/core/lib/pages/login/login.scss +140 -32
  15. package/core/models/dashboard/dashboard.js +14 -0
  16. package/core/models/doctor/components/doctor-add/doctor-add.js +403 -0
  17. package/core/models/doctor/components/doctor-add/doctor-add.scss +32 -0
  18. package/core/models/menus/components/menu-add/menu-add.js +220 -267
  19. package/core/models/menus/components/menu-lists/menu-lists.js +366 -211
  20. package/core/models/menus/components/menu-lists/menu-lists.scss +6 -2
  21. package/core/models/menus/menus.js +256 -267
  22. package/core/models/roles/components/role-add/role-add.js +265 -228
  23. package/core/models/roles/components/role-list/role-list.js +326 -348
  24. package/core/models/roles/roles.js +191 -174
  25. package/core/models/staff/components/staff-add/staff-add.js +352 -0
  26. package/core/models/staff/components/staff-add/staff-add.scss +0 -0
  27. package/core/models/users/components/user-add/user-add.js +723 -367
  28. package/core/models/users/components/user-add/user-edit.js +90 -0
  29. package/core/models/users/users.js +318 -165
  30. package/core/modules/index.js +5 -8
  31. package/core/modules/reporting/components/index.js +5 -0
  32. package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +65 -2
  33. package/core/modules/steps/action-buttons.js +79 -0
  34. package/core/modules/steps/steps.js +553 -0
  35. package/core/modules/steps/steps.scss +158 -0
  36. package/core/modules/steps/timeline.js +49 -0
  37. package/package.json +2 -2
@@ -43,8 +43,8 @@
43
43
  width: 17%;
44
44
  background: #fff;
45
45
  // border-bottom: 1.5px solid #24aeb8;
46
- &.close{
47
- width:6% !important
46
+ &.close {
47
+ width: 6% !important;
48
48
  }
49
49
 
50
50
  .logo-wrapper {
@@ -70,7 +70,7 @@
70
70
 
71
71
  @media only screen and (max-width: 500px) {
72
72
  margin-right: 0px;
73
- // margin: 10px;
73
+ // margin: 10px;
74
74
  width: 150px;
75
75
  }
76
76
 
@@ -160,7 +160,7 @@
160
160
 
161
161
  /* Pseudo-element (as before) */
162
162
  .menu-collapsed::after {
163
- content: "";
163
+ content: '';
164
164
  position: absolute;
165
165
  top: -100%;
166
166
  left: 0;
@@ -175,26 +175,26 @@
175
175
  // background-color: #E0EBFF;
176
176
  // transform: scale(0.95); /* Apply scale effect on hover */
177
177
  }
178
- /* Style selected item */
179
- .ant-menu{
180
- background-color: transparent !important;
181
- }
182
- // .ant-menu-inline .ant-menu-item::after{
183
- // border-right: none;
184
- // }
185
- // .ant-menu-item-selected {
186
- // background-color: var(--selected-bg-color) !important;
187
- // color: var(--selected-text-color) !important;
188
- // border-radius: 12px;
189
- // // font-weight: 600;
190
- // // margin: 4px 8px;
191
- // }
192
-
193
- // /* Optional: remove hover effects */
194
- // .ant-menu-item:hover {
195
- // background-color: transparent !important;
196
- // color: transparent !important;
197
- // }
178
+ /* Style selected item */
179
+ .ant-menu {
180
+ background-color: transparent !important;
181
+ }
182
+ // .ant-menu-inline .ant-menu-item::after{
183
+ // border-right: none;
184
+ // }
185
+ // .ant-menu-item-selected {
186
+ // background-color: var(--selected-bg-color) !important;
187
+ // color: var(--selected-text-color) !important;
188
+ // border-radius: 12px;
189
+ // // font-weight: 600;
190
+ // // margin: 4px 8px;
191
+ // }
192
+
193
+ // /* Optional: remove hover effects */
194
+ // .ant-menu-item:hover {
195
+ // background-color: transparent !important;
196
+ // color: transparent !important;
197
+ // }
198
198
 
199
199
  /* River flow effect */
200
200
  .menu-collapsed:hover::after {
@@ -248,9 +248,7 @@
248
248
  padding: 0px;
249
249
  // width: 100% !important;
250
250
  // padding: 10px 16px;
251
-
252
251
  }
253
-
254
252
 
255
253
  .menu-item {
256
254
  width: 100% !important;
@@ -262,3 +260,18 @@
262
260
  }
263
261
  }
264
262
  }
263
+
264
+ .ant-menu-item:hover {
265
+ background-color: #e6f7ff !important;
266
+ color: #1677ff !important;
267
+ }
268
+
269
+ /* Tooltip styling if needed */
270
+ .ant-tooltip-inner {
271
+ max-width: 200px;
272
+ white-space: nowrap;
273
+ overflow: hidden;
274
+ text-overflow: ellipsis;
275
+ background-color: #ffffff !important;
276
+ color: #000000 !important;
277
+ }
@@ -1,61 +1,138 @@
1
- import React, { useRef } from "react";
2
- import { useDrag, useDrop } from "react-dnd";
1
+ import React, { useRef } from 'react';
2
+ import { useDrag, useDrop } from 'react-dnd';
3
3
 
4
- const ItemTypes = { PANEL: "panel" };
4
+ export default function DraggableWrapper({ id, index, movePanel, item, dragEnabled, level, parentId, onCrossLevelMove, canAcceptChildren }) {
5
5
 
6
- export default function DraggableWrapper({ id, index, movePanel, title, dragEnabled = true }) {
7
- const ref = useRef(null);
6
+ const autoScrollWindow = (monitor) => {
7
+ const offset = monitor.getClientOffset();
8
+ if (!offset) return;
8
9
 
9
- const [, drop] = useDrop({
10
- accept: ItemTypes.PANEL,
11
- hover(item, monitor) {
12
- if (!dragEnabled) return; // ignore hover if dragging is disabled
13
- if (!ref.current) return;
10
+ const EDGE = 80;
11
+ const SPEED = 20;
14
12
 
15
- const dragIndex = item.index;
16
- const hoverIndex = index;
13
+ const viewportHeight = window.innerHeight;
17
14
 
18
- if (dragIndex === hoverIndex) return;
15
+ // 🔼 scroll UP
16
+ if (offset.y < EDGE) {
17
+ window.scrollBy(0, -SPEED);
18
+ }
19
19
 
20
- const hoverBoundingRect = ref.current.getBoundingClientRect();
21
- const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
22
- const clientOffset = monitor.getClientOffset();
23
- const hoverClientY = clientOffset.y - hoverBoundingRect.top;
20
+ // 🔽 scroll DOWN
21
+ if (offset.y > viewportHeight - EDGE) {
22
+ window.scrollBy(0, SPEED);
23
+ }
24
+ };
24
25
 
25
- if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return;
26
- if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return;
26
+ const [{ isDragging }, drag] = useDrag({
27
+ type: 'PANEL',
28
+ item: { id, index, level, parentId },
29
+ canDrag: dragEnabled,
30
+ collect: (monitor) => ({
31
+ isDragging: monitor.isDragging(),
32
+ }),
33
+ });
27
34
 
28
- movePanel(dragIndex, hoverIndex);
29
- item.index = hoverIndex;
30
- },
35
+ const [{ isOver, canDrop }, drop] = useDrop({
36
+ accept: 'PANEL',
37
+ hover: (dragItem, monitor) => {
38
+ // THIS FIXES BOTTOM → TOP
39
+ autoScrollWindow(monitor);
40
+
41
+ if (dragItem.index === index) return;
42
+
43
+ if (
44
+ dragItem.level === level &&
45
+ dragItem.parentId === parentId
46
+ ) {
47
+ movePanel(dragItem.index, index);
48
+ dragItem.index = index; // keep in sync
49
+ }
50
+ },
51
+
52
+ canDrop: (item) => dragEnabled,
53
+ drop: (dragItem, monitor) => {
54
+ if (monitor.didDrop()) return;
55
+
56
+ if (
57
+ dragItem.level !== level ||
58
+ dragItem.parentId !== parentId
59
+ ) {
60
+ onCrossLevelMove?.(dragItem, {
61
+ targetLevel: level,
62
+ targetParentId: parentId,
63
+ targetIndex: index,
64
+ });
65
+ }
66
+ },
67
+ collect: (monitor) => ({
68
+ isOver: monitor.isOver({ shallow: true }),
69
+ canDrop: monitor.canDrop(),
70
+ }),
31
71
  });
32
72
 
33
- const [{ isDragging }, drag] = useDrag({
34
- type: ItemTypes.PANEL,
35
- item: { id, index },
36
- canDrag: dragEnabled, // only allow dragging if dragEnabled
73
+ // Drop zone for making items children of this item
74
+ const [{ isOverChild, canDropChild }, dropChild] = useDrop({
75
+ accept: 'PANEL',
76
+ canDrop: (item) => dragEnabled && item.id !== id && canAcceptChildren,
77
+ drop: (item, monitor) => {
78
+ if (monitor.didDrop()) return;
79
+
80
+ if (onCrossLevelMove && item.id !== id) {
81
+ // Drop as child of this item
82
+ onCrossLevelMove(item, { targetLevel: level + 1, targetParentId: id, targetIndex: 0 });
83
+ }
84
+ },
37
85
  collect: (monitor) => ({
38
- isDragging: monitor.isDragging(),
86
+ isOverChild: monitor.isOver({ shallow: true }),
87
+ canDropChild: monitor.canDrop(),
39
88
  }),
40
89
  });
41
90
 
42
- drag(drop(ref));
91
+ const backgroundColor = isOver && canDrop ? '#bae7ff' : isDragging ? '#e6f7ff' : 'transparent';
92
+ const childZoneBackgroundColor = isOverChild && canDropChild ? '#d4f4dd' : 'transparent';
43
93
 
44
94
  return (
45
- <div
46
- ref={ref}
47
- style={{
48
- display: "flex",
49
- width: "100%",
50
- cursor: dragEnabled ? "grab" : "default", // show cursor only if draggable
51
- opacity: isDragging ? 0.6 : 1,
52
- transition: "transform 0.2s ease, opacity 0.2s ease",
53
- transform: isDragging ? "scale(1.05)" : "scale(1)",
54
- boxShadow: isDragging ? "0px 5px 10px rgba(0,0,0,0.15)" : "none",
55
- padding: "5px 0",
56
- }}
57
- >
58
- {title}
95
+ <div ref={drop} style={{ width: '100%' }}>
96
+ <div
97
+ ref={drag}
98
+ style={{
99
+ opacity: isDragging ? 0.5 : 1,
100
+ cursor: dragEnabled ? 'move' : 'default',
101
+ // padding: '8px',
102
+ backgroundColor,
103
+ transition: 'all 0.2s ease',
104
+ border: isOver && canDrop ? '2px dashed #1890ff' : '2px solid transparent',
105
+ display: 'flex',
106
+ justifyContent: 'space-between',
107
+ alignItems: 'center',
108
+ }}
109
+ >
110
+ <div style={{ flex: 1 }}>
111
+ {dragEnabled && <span style={{ marginRight: 8, color: '#999' }}>⋮⋮</span>}
112
+ <strong>{item.name}</strong>
113
+ {dragEnabled ?( <span style={{ marginLeft: 8, fontSize: 11, color: '#999' }}>(Level {level})</span>):(<span style={{ marginLeft: 8, fontSize: 11, color: '#999' }}>{item.path}</span>)}
114
+ </div>
115
+
116
+ {canAcceptChildren && dragEnabled && (
117
+ <div
118
+ ref={dropChild}
119
+ onClick={(e) => e.stopPropagation()}
120
+ style={{
121
+ padding: '4px 12px',
122
+ marginLeft: 8,
123
+ backgroundColor: childZoneBackgroundColor,
124
+ border: isOverChild && canDropChild ? '2px solid #52c41a' : '1px dashed #d9d9d9',
125
+ borderRadius: 4,
126
+ fontSize: 11,
127
+ color: isOverChild && canDropChild ? '#52c41a' : '#999',
128
+ cursor: 'default',
129
+ transition: 'all 0.2s ease',
130
+ }}
131
+ >
132
+ {isOverChild && canDropChild ? '✓ Drop as child' : '↳ Drop here'}
133
+ </div>
134
+ )}
135
+ </div>
59
136
  </div>
60
137
  );
61
138
  }
@@ -1,46 +1,135 @@
1
- import { DatePicker } from 'antd';
1
+ /**
2
+ * @file rangepicker.js
3
+ * @description A reusable, enhanced Ant Design RangePicker component.
4
+ *
5
+ * Features:
6
+ * - Manual selections require "Apply" button confirmation
7
+ * - Preset ranges apply immediately without confirmation
8
+ * - Optimized state management and event handling
9
+ */
10
+
11
+ import { useCallback, useRef, useState } from 'react';
12
+ import { DatePicker, Space } from 'antd';
2
13
  import moment from 'moment-timezone';
3
14
  import PropTypes from 'prop-types';
15
+ import { useTranslation } from 'react-i18next';
16
+ import Button from '../button/button';
4
17
  import './rangepicker.scss';
5
18
 
19
+ /**
20
+ * Enhanced RangePicker with Apply/Cancel controls for manual selections.
21
+ * @param {object} props
22
+ * @param {moment.Moment[]} props.value - Current applied date range
23
+ * @param {function(moment.Moment[]): void} props.onChange - Callback when range is applied
24
+ * @param {string} [props.format='DD/MM/YYYY'] - Date display format
25
+ * @param {object} [props.ranges] - Preset date ranges
26
+ * @param {boolean} [props.allowClear=false] - Whether to show clear button
27
+ * @param {boolean} [props.inputReadOnly=true] - Whether input is read-only
28
+ */
29
+
6
30
  const { RangePicker } = DatePicker;
7
31
 
8
- export default function RangePickerComponent({ onChange, value, ranges }) {
9
- // value is still: [moment, moment]
10
- const startDate = value[0];
11
- const endDate = value[1];
32
+ export default function RangePickerComponent({
33
+ value,
34
+ onChange,
35
+ format = 'DD/MM/YYYY',
36
+ ranges,
37
+ allowClear = false,
38
+ inputReadOnly = true,
39
+ ...restProps
40
+ }) {
41
+ const { t } = useTranslation();
42
+
43
+ // Temporary range during selection (before Apply is clicked)
44
+ const [tempRange, setTempRange] = useState(null);
45
+ const [isOpen, setIsOpen] = useState(false);
46
+
47
+ // Ref to track if a selection just happened, to prevent the picker from closing.
48
+ const selectionJustHappened = useRef(false);
12
49
 
13
- function handleChange(dates) {
14
- if (!dates) {
15
- onChange(null);
50
+ /**
51
+ * Handles any completed date selection (both manual and preset).
52
+ * It updates the temporary range and sets a flag to keep the picker open.
53
+ */
54
+ const handleChange = useCallback((dates) => {
55
+ setTempRange(dates || []);
56
+ selectionJustHappened.current = true;
57
+ }, []);
58
+
59
+ /**
60
+ * Handles picker open/close events.
61
+ * Prevents closing after a selection, requiring user to click Apply/Cancel.
62
+ */
63
+ const handleOpenChange = useCallback((open) => {
64
+ // If the picker is trying to close, check if it was due to a selection.
65
+ if (!open && selectionJustHappened.current) {
66
+ // It was a selection, so ignore the close request and reset the flag.
67
+ selectionJustHappened.current = false;
16
68
  return;
17
69
  }
18
70
 
19
- // Convert dayjs moment so your external code does NOT break
20
- const converted = [moment(dates[0].toDate()), moment(dates[1].toDate())];
71
+ // For all other cases (opening, or closing by clicking outside), proceed as normal.
72
+ if (open) {
73
+ // Reset temp range when opening for a fresh selection.
74
+ setTempRange(null);
75
+ }
76
+ setIsOpen(open);
77
+ }, []);
78
+
79
+ /**
80
+ * Applies the temporary range and closes picker
81
+ */
82
+ const handleApply = useCallback(() => {
83
+ if (tempRange?.length === 2 && onChange) {
84
+ onChange(tempRange);
85
+ }
86
+ selectionJustHappened.current = false; // Clear the flag
87
+ setIsOpen(false);
88
+ }, [tempRange, onChange]);
21
89
 
22
- onChange(converted);
23
- }
90
+ /**
91
+ * Cancels selection and closes picker
92
+ */
93
+ const handleCancel = useCallback(() => {
94
+ selectionJustHappened.current = false; // Clear the flag
95
+ setTempRange(null);
96
+ setIsOpen(false);
97
+ }, []);
24
98
 
25
- // Convert your old "ranges" object into AntD presets array
26
- const presets = Object.entries(ranges).map(([label, range]) => ({
27
- label,
28
- value: range,
29
- }));
99
+ // Memoized preset ranges - use provided ranges or default set
100
+ const defaultRanges = useRef({
101
+ Today: [moment(), moment()],
102
+ Yesterday: [moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').endOf('day')],
103
+ 'This Week': [moment().startOf('week'), moment().endOf('week')],
104
+ 'Last Week': [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')],
105
+ 'This Month': [moment().startOf('month'), moment().endOf('month')],
106
+ 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')],
107
+ }).current;
30
108
 
31
109
  return (
32
- <div className="rangepicker">
33
- <RangePicker
34
- allowClear={false}
35
- inputReadOnly
36
- format="DD/MM/YYYY"
37
- // moment → allowed
38
- value={[startDate, endDate]}
39
- onChange={handleChange}
40
- presets={presets}
41
- ranges={ranges}
42
- />
43
- </div>
110
+ <RangePicker
111
+ allowClear={allowClear}
112
+ inputReadOnly={inputReadOnly}
113
+ format={format}
114
+ value={tempRange || value}
115
+ open={isOpen}
116
+ onOpenChange={handleOpenChange}
117
+ onChange={handleChange}
118
+ ranges={ranges !== undefined ? ranges : defaultRanges}
119
+ {...restProps}
120
+ renderExtraFooter={() => (
121
+ <div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end', order: 1 }}>
122
+ <Space>
123
+ <Button size="small" onClick={handleCancel}>
124
+ {t('Cancel')}
125
+ </Button>
126
+ <Button type="primary" size="small" onClick={handleApply}>
127
+ {t('Apply')}
128
+ </Button>
129
+ </Space>
130
+ </div>
131
+ )}
132
+ />
44
133
  );
45
134
  }
46
135
 
@@ -1,36 +1,46 @@
1
1
  /**
2
2
  * Component for antd switch
3
3
  */
4
-
5
- import React, { useState, useContext, useEffect } from 'react';
6
- import { GlobalContext } from '../../../Store';
7
-
4
+ import React, { useContext } from 'react';
8
5
  import { Switch as AntdSwitch } from 'antd';
9
-
6
+ import { GlobalContext } from '../../../Store';
10
7
  import PropTypes from 'prop-types';
11
8
 
12
- export default function Switch({ onChange, checked }) {
9
+ export default function Switch({
10
+ checked,
11
+ defaultChecked,
12
+ onChange,
13
+ disabled,
14
+ id,
15
+ size,
16
+ checkedChildren,
17
+ unCheckedChildren,
18
+ className,
19
+ style,
20
+ }) {
13
21
  const { state } = useContext(GlobalContext);
14
22
 
15
- useEffect(() => {
16
- // Reacting to theme changes
17
- }, [state.theme.colors]);
18
-
19
23
  return (
20
- <div>
21
- <>
22
- <AntdSwitch
23
- checked={checked}
24
- onChange={onChange}
25
- // checked={ }
26
- style={{
27
- backgroundColor: checked ? state.theme.colors.primaryButtonBg : state.theme.colors.primaryButtonDisabledBg, // Use disabled color when unchecked
28
- borderColor: checked ? state.theme.colors.primaryButtonBg : state.theme.colors.primaryButtonDisabledBg,
29
- }}
30
- listType="picture-card"
31
- />
32
- </>
33
- </div>
24
+ <AntdSwitch
25
+ id={id}
26
+ checked={checked}
27
+ defaultChecked={defaultChecked}
28
+ onChange={onChange}
29
+ disabled={disabled}
30
+ size={size}
31
+ checkedChildren={checkedChildren}
32
+ unCheckedChildren={unCheckedChildren}
33
+ className={className}
34
+ style={{
35
+ backgroundColor: checked
36
+ ? state.theme.colors.primaryButtonBg
37
+ : state.theme.colors.primaryButtonDisabledBg,
38
+ borderColor: checked
39
+ ? state.theme.colors.primaryButtonBg
40
+ : state.theme.colors.primaryButtonDisabledBg,
41
+ ...style,
42
+ }}
43
+ />
34
44
  );
35
45
  }
36
46
 
@@ -55,4 +65,4 @@ Switch.propTypes = {
55
65
  className: PropTypes.string,
56
66
  /** A custom style object for the switch component. */
57
67
  style: PropTypes.object,
58
- };
68
+ };
@@ -1,19 +1,9 @@
1
-
2
-
3
-
4
1
  import useDeviceDetect from './device-detect';
5
2
 
6
3
  import useWindowSize from './use-window-size';
7
4
 
8
5
  import useFormUtils from './../utils/form/form.utils';
9
6
  import useLocation from './use-location';
7
+ import { useOtpTimer } from './use-otp-timer';
10
8
 
11
-
12
- export {
13
- useDeviceDetect,
14
- useWindowSize,
15
-
16
-
17
- useFormUtils,
18
- useLocation
19
- }
9
+ export { useDeviceDetect, useWindowSize, useOtpTimer, useFormUtils, useLocation };
@@ -0,0 +1,99 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+
3
+ /**
4
+ * Custom hook to manage an OTP (One-Time Password) countdown timer.
5
+ *
6
+ * Features:
7
+ * - Can start a timer directly with seconds.
8
+ * - Can start a timer using an expiry timestamp (from backend).
9
+ * - Provides formatted remaining time (MM:SS).
10
+ * - Tracks whether the timer has expired.
11
+ *
12
+ * @returns {Object} API
13
+ * @returns {number} API.remaining - Remaining seconds.
14
+ * @returns {boolean} API.expired - Whether the timer has expired.
15
+ * @returns {string} API.formatted - Formatted remaining time (MM:SS).
16
+ * @returns {(seconds: number) => void} API.start - Start timer with seconds.
17
+ * @returns {(expirytime: string) => void} API.startFromExpiry - Start timer using expiry string (e.g. "2025-09-04T12:13:09.000Z").
18
+ *
19
+ * @example
20
+ * const { remaining, expired, formatted, start, startFromExpiry } = useOtpTimer();
21
+ *
22
+ * // Start with 30 seconds
23
+ * useEffect(() => {
24
+ * start(30);
25
+ * }, []);
26
+ *
27
+ * // OR start from backend expiry timestamp
28
+ * useEffect(() => {
29
+ * startFromExpiry("2025-09-04T12:13:09.000Z");
30
+ * }, []);
31
+ *
32
+ * return (
33
+ * <div>
34
+ * <p>Time left: {formatted}</p>
35
+ * {expired && <p>OTP expired!</p>}
36
+ * </div>
37
+ * );
38
+ */
39
+
40
+ // helper to format time
41
+ const formatTime = (seconds) => {
42
+ const minutes = Math.floor((seconds % 3600) / 60);
43
+ const secs = seconds % 60;
44
+ return `${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
45
+ };
46
+
47
+ export const useOtpTimer = () => {
48
+ const [remaining, setRemaining] = useState(0);
49
+ const [expired, setExpired] = useState(false);
50
+ const intervalRef = useRef(null);
51
+
52
+ // Start timer by passing expiry string from backend
53
+ const startFromExpiry = (expirytime) => {
54
+ if (!expirytime) {
55
+ setRemaining(0);
56
+ setExpired(true);
57
+ return;
58
+ }
59
+
60
+ // Handle backend expiry string
61
+ const expiryTime = new Date(expirytime).getTime();
62
+ const now = Date.now();
63
+ const seconds = Math.max(0, Math.floor((expiryTime - now) / 1000));
64
+
65
+ start(seconds);
66
+ };
67
+
68
+ // Generic start (seconds)
69
+ const start = (seconds) => {
70
+ clearInterval(intervalRef.current);
71
+
72
+ if (!seconds || seconds <= 0) {
73
+ setRemaining(0);
74
+ setExpired(true);
75
+ return;
76
+ }
77
+
78
+ setRemaining(seconds);
79
+ setExpired(false);
80
+
81
+ intervalRef.current = setInterval(() => {
82
+ setRemaining((prev) => {
83
+ if (prev <= 1) {
84
+ clearInterval(intervalRef.current);
85
+ setExpired(true);
86
+ return 0;
87
+ }
88
+ return prev - 1;
89
+ });
90
+ }, 1000);
91
+ };
92
+
93
+ // Cleanup on unmount
94
+ useEffect(() => {
95
+ return () => clearInterval(intervalRef.current);
96
+ }, []);
97
+
98
+ return { remaining, expired, formatted: formatTime(remaining), start, startFromExpiry };
99
+ };