ui-soxo-bootstrap-core 2.4.26 → 2.5.0
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/core/components/external-window/DEVELOPER_GUIDE.md +705 -0
- package/core/components/external-window/external-window.js +225 -0
- package/core/components/external-window/external-window.test.js +80 -0
- package/core/components/index.js +4 -1
- package/core/components/landing-api/landing-api.js +18 -18
- package/core/lib/Store.js +20 -18
- package/core/lib/components/index.js +4 -1
- package/core/lib/elements/basic/rangepicker/rangepicker.js +118 -29
- package/core/lib/elements/basic/switch/switch.js +34 -24
- package/core/models/dashboard/dashboard.js +14 -0
- package/core/modules/index.js +2 -0
- package/core/modules/steps/action-buttons.js +88 -0
- package/core/modules/steps/steps.js +332 -0
- package/core/modules/steps/steps.scss +158 -0
- package/core/modules/steps/timeline.js +54 -0
- package/jest.config.js +8 -0
- package/jest.setup.js +1 -0
- package/package.json +9 -4
|
@@ -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({
|
|
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
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
|
@@ -81,6 +81,20 @@ class Dashboard extends Base {
|
|
|
81
81
|
];
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
loadProcess(id) {
|
|
85
|
+
return ApiUtils.get({
|
|
86
|
+
url: `process/${id}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
processLog(formBody) {
|
|
91
|
+
return ApiUtils.post({
|
|
92
|
+
url: `process/process-log`,
|
|
93
|
+
formBody,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
84
98
|
/**
|
|
85
99
|
* Function to load dashboards based on user information
|
|
86
100
|
* @param {*} user
|
package/core/modules/index.js
CHANGED
|
@@ -22,8 +22,10 @@ import ReportingDashboard from '../modules/reporting/components/reporting-dashbo
|
|
|
22
22
|
|
|
23
23
|
import ChangeInfo from './Informations/change-info/change-info';
|
|
24
24
|
// All Dashboard Components Ends
|
|
25
|
+
import ProcessStepsPage from './steps/steps';
|
|
25
26
|
|
|
26
27
|
export {
|
|
28
|
+
ProcessStepsPage,
|
|
27
29
|
GenericList,
|
|
28
30
|
GenericAdd,
|
|
29
31
|
GenericEdit,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionButtons
|
|
3
|
+
* Handles navigation and action controls for a multi-step process,
|
|
4
|
+
* including dynamic content rendering and process completion actions.
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { Skeleton } from 'antd';
|
|
8
|
+
import { Button } from '../../lib';
|
|
9
|
+
|
|
10
|
+
export default function ActionButtons({
|
|
11
|
+
loading,
|
|
12
|
+
steps,
|
|
13
|
+
activeStep,
|
|
14
|
+
isStepCompleted,
|
|
15
|
+
renderDynamicComponent,
|
|
16
|
+
handlePrevious,
|
|
17
|
+
handleNext,
|
|
18
|
+
handleSkip,
|
|
19
|
+
handleFinish,
|
|
20
|
+
handleStartNextProcess,
|
|
21
|
+
nextProcessId,
|
|
22
|
+
timelineCollapsed,
|
|
23
|
+
}) {
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<div style={{ minHeight: 300 }}>{loading ? <Skeleton active /> : renderDynamicComponent()}</div>
|
|
27
|
+
<>
|
|
28
|
+
<div style={{ marginTop: 20, display: 'flex', justifyContent: 'flex-start', gap: '10px' }}>
|
|
29
|
+
{/* Back button */}
|
|
30
|
+
<Button disabled={activeStep === 0} onClick={handlePrevious} style={{ marginRight: 8, borderRadius: 4 }}>
|
|
31
|
+
Back
|
|
32
|
+
</Button>
|
|
33
|
+
|
|
34
|
+
{/* Skip button */}
|
|
35
|
+
{steps.length > 0 && steps[activeStep]?.allow_skip === 'Y' && (
|
|
36
|
+
<Button
|
|
37
|
+
type="default"
|
|
38
|
+
onClick={handleSkip}
|
|
39
|
+
style={{
|
|
40
|
+
borderRadius: 4,
|
|
41
|
+
}}
|
|
42
|
+
disabled={activeStep === steps.length - 1}
|
|
43
|
+
>
|
|
44
|
+
Skip
|
|
45
|
+
</Button>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{/* Next / Finish / Start Next */}
|
|
49
|
+
{steps[activeStep]?.order_seqtype === 'E' ? (
|
|
50
|
+
nextProcessId?.next_process_id ? (
|
|
51
|
+
<Button
|
|
52
|
+
type="primary"
|
|
53
|
+
style={{
|
|
54
|
+
borderRadius: 4,
|
|
55
|
+
}}
|
|
56
|
+
onClick={handleStartNextProcess}
|
|
57
|
+
>
|
|
58
|
+
Start Next {nextProcessId.next_process_name}
|
|
59
|
+
</Button>
|
|
60
|
+
) : (
|
|
61
|
+
<Button
|
|
62
|
+
type="primary"
|
|
63
|
+
style={{
|
|
64
|
+
borderRadius: 4,
|
|
65
|
+
}}
|
|
66
|
+
onClick={handleFinish}
|
|
67
|
+
>
|
|
68
|
+
Finish
|
|
69
|
+
</Button>
|
|
70
|
+
)
|
|
71
|
+
) : (
|
|
72
|
+
<Button
|
|
73
|
+
type="primary"
|
|
74
|
+
// shape="round"
|
|
75
|
+
style={{
|
|
76
|
+
borderRadius: 4,
|
|
77
|
+
}}
|
|
78
|
+
disabled={activeStep === steps.length - 1 || !isStepCompleted}
|
|
79
|
+
onClick={handleNext}
|
|
80
|
+
>
|
|
81
|
+
Next →
|
|
82
|
+
</Button>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProcessStepsPage Component
|
|
3
|
+
*
|
|
4
|
+
* - Manages a multi-step, time-tracked process workflow.
|
|
5
|
+
* - Dynamically renders step-specific components based on configuration.
|
|
6
|
+
* - Tracks step and process durations with local persistence support.
|
|
7
|
+
* - Supports step navigation (next, previous, skip, timeline, keyboard).
|
|
8
|
+
* - Handles process submission and optional chaining to the next process.
|
|
9
|
+
* - Provides a collapsible timeline view and action controls.
|
|
10
|
+
*/
|
|
11
|
+
import React, { useEffect, useState } from 'react';
|
|
12
|
+
import { Row, Col, Empty } from 'antd';
|
|
13
|
+
import { Card } from './../../lib';
|
|
14
|
+
import * as genericComponents from './../../lib';
|
|
15
|
+
import moment from 'moment';
|
|
16
|
+
import { Location } from './../../lib';
|
|
17
|
+
import ActionButtons from './action-buttons';
|
|
18
|
+
import { Dashboard } from '../../models';
|
|
19
|
+
import './steps.scss';
|
|
20
|
+
import TimelinePanel from './timeline';
|
|
21
|
+
import { ExternalWindow } from '../../components';
|
|
22
|
+
|
|
23
|
+
export default function ProcessStepsPage({ processId, match, CustomComponents = {}, ...props }) {
|
|
24
|
+
const allComponents = { ...genericComponents, ...CustomComponents };
|
|
25
|
+
|
|
26
|
+
const [loading, setLoading] = useState(false);
|
|
27
|
+
const [steps, setSteps] = useState([]);
|
|
28
|
+
const [activeStep, setActiveStep] = useState(0);
|
|
29
|
+
const [isStepCompleted, setIsStepCompleted] = useState(false);
|
|
30
|
+
const [currentProcessId, setCurrentProcessId] = useState(processId);
|
|
31
|
+
const [nextProcessId, setNextProcessId] = useState(null);
|
|
32
|
+
const [stepStartTime, setStepStartTime] = useState(null);
|
|
33
|
+
const [processStartTime, setProcessStartTime] = useState(null);
|
|
34
|
+
const [processTimings, setProcessTimings] = useState([]);
|
|
35
|
+
const [timelineCollapsed, setTimelineCollapsed] = useState(true);
|
|
36
|
+
const [showExternalWindow, setShowExternalWindow] = useState(false);
|
|
37
|
+
const urlParams = Location.search();
|
|
38
|
+
|
|
39
|
+
// Load process details based on the current process ID
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
loadProcess(currentProcessId);
|
|
42
|
+
|
|
43
|
+
const saved = localStorage.getItem(`processTimings_${currentProcessId}`);
|
|
44
|
+
setProcessTimings(saved ? JSON.parse(saved) : []);
|
|
45
|
+
|
|
46
|
+
setProcessStartTime(Date.now());
|
|
47
|
+
setStepStartTime(Date.now());
|
|
48
|
+
}, [currentProcessId]);
|
|
49
|
+
|
|
50
|
+
//// Reset step start time whenever the active step changes
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
setStepStartTime(Date.now());
|
|
54
|
+
}, [activeStep]);
|
|
55
|
+
|
|
56
|
+
// Check whether the current step is completed or mandatory
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (steps.length > 0) {
|
|
59
|
+
setIsStepCompleted(steps[activeStep]?.is_mandatory !== true);
|
|
60
|
+
}
|
|
61
|
+
}, [activeStep, steps]);
|
|
62
|
+
|
|
63
|
+
// Save updated process timings to state and localStorage
|
|
64
|
+
const saveTimings = (updated) => {
|
|
65
|
+
setProcessTimings(updated);
|
|
66
|
+
localStorage.setItem(`processTimings_${currentProcessId}`, JSON.stringify(updated));
|
|
67
|
+
};
|
|
68
|
+
// Record time spent on the current step
|
|
69
|
+
|
|
70
|
+
const recordStepTime = (status = 'completed') => {
|
|
71
|
+
// Exit if step start time or step data is missing
|
|
72
|
+
|
|
73
|
+
if (!stepStartTime || !steps[activeStep]) return processTimings;
|
|
74
|
+
// Capture end time and calculate duration
|
|
75
|
+
|
|
76
|
+
const endTime = Date.now();
|
|
77
|
+
const duration = endTime - stepStartTime;
|
|
78
|
+
const stepId = steps[activeStep].step_id;
|
|
79
|
+
// Clone existing timings
|
|
80
|
+
|
|
81
|
+
const updated = [...processTimings];
|
|
82
|
+
const index = updated.findIndex((t) => t.step_id === stepId);
|
|
83
|
+
// Create timing entry for the step
|
|
84
|
+
|
|
85
|
+
const entry = {
|
|
86
|
+
step_id: stepId,
|
|
87
|
+
start_time: moment(stepStartTime).format('DD-MM-YYYY HH:mm:ss'),
|
|
88
|
+
end_time: moment(endTime).format('DD-MM-YYYY HH:mm:ss'),
|
|
89
|
+
duration,
|
|
90
|
+
status,
|
|
91
|
+
};
|
|
92
|
+
// Update existing entry or add a new one
|
|
93
|
+
if (index > -1) {
|
|
94
|
+
updated[index] = { ...updated[index], ...entry, duration: updated[index].duration + duration };
|
|
95
|
+
} else {
|
|
96
|
+
updated.push(entry);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return updated;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {*} processId
|
|
104
|
+
*
|
|
105
|
+
* Process Loading
|
|
106
|
+
* - Fetches process details and step configuration using the process ID.
|
|
107
|
+
* - Manages loading state during the API call.
|
|
108
|
+
* - Stores step data and prepares next process details if available.
|
|
109
|
+
* - Handles API errors and maintains UI stability.
|
|
110
|
+
*/
|
|
111
|
+
async function loadProcess(processId) {
|
|
112
|
+
setLoading(true);
|
|
113
|
+
setNextProcessId(null);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const result = await Dashboard.loadProcess(processId);
|
|
117
|
+
|
|
118
|
+
setSteps(result?.data?.steps || []);
|
|
119
|
+
if (result?.data?.next_process_id) setNextProcessId(result.data);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.error('Error loading process steps:', e);
|
|
122
|
+
} finally {
|
|
123
|
+
setLoading(false);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* @param {*} finalTimings
|
|
128
|
+
*
|
|
129
|
+
* Process Submission
|
|
130
|
+
* - Builds payload with process metadata, reference details, and step timings.
|
|
131
|
+
* - Submits process completion data to the backend.
|
|
132
|
+
* - Clears stored timings on successful submission.
|
|
133
|
+
* - Persists timing data locally if submission fails.
|
|
134
|
+
*/
|
|
135
|
+
const handleProcessSubmit = async (finalTimings) => {
|
|
136
|
+
const payload = {
|
|
137
|
+
process_id: currentProcessId,
|
|
138
|
+
status: 'completed',
|
|
139
|
+
reference_id: urlParams?.opb_id || urlParams?.reference_id,
|
|
140
|
+
reference_number: urlParams?.opno || urlParams?.reference_number,
|
|
141
|
+
mode: urlParams?.mode,
|
|
142
|
+
process: {
|
|
143
|
+
process_start_time: moment(processStartTime).format('DD-MM-YYYY HH:mm'),
|
|
144
|
+
process_end_time: moment().format('DD-MM-YYYY HH:mm'),
|
|
145
|
+
steps: finalTimings,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const response = await Dashboard.processLog(payload);
|
|
151
|
+
|
|
152
|
+
if (response.success) {
|
|
153
|
+
localStorage.removeItem(`processTimings_${currentProcessId}`);
|
|
154
|
+
setProcessTimings([]);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.error('Error:', e);
|
|
159
|
+
saveTimings(finalTimings);
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* @param {number} index
|
|
165
|
+
* @param {string} status
|
|
166
|
+
*
|
|
167
|
+
* Step Navigation
|
|
168
|
+
* - Records time spent on the current step.
|
|
169
|
+
* - Saves updated step timing data.
|
|
170
|
+
* - Navigates to the specified step index.
|
|
171
|
+
*/
|
|
172
|
+
const gotoStep = (index, status = 'completed') => {
|
|
173
|
+
const updated = recordStepTime(status);
|
|
174
|
+
saveTimings(updated);
|
|
175
|
+
setActiveStep(index);
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Navigate to the next step
|
|
179
|
+
* - Records timing data and advances step index by one.
|
|
180
|
+
*/
|
|
181
|
+
const handleNext = () => gotoStep(activeStep + 1);
|
|
182
|
+
/**
|
|
183
|
+
* Navigate to the previous step
|
|
184
|
+
* - Records timing data and moves to the previous step.
|
|
185
|
+
*/
|
|
186
|
+
const handlePrevious = () => gotoStep(activeStep - 1);
|
|
187
|
+
/**
|
|
188
|
+
* Skip current step
|
|
189
|
+
* - Records timing with skipped status.
|
|
190
|
+
* - Moves to the next step.
|
|
191
|
+
*/
|
|
192
|
+
const handleSkip = () => gotoStep(activeStep + 1, 'skipped');
|
|
193
|
+
/**
|
|
194
|
+
* Timeline Navigation
|
|
195
|
+
* - Navigates directly to the selected step.
|
|
196
|
+
* - Records timing data for the current step.
|
|
197
|
+
*/
|
|
198
|
+
const handleTimelineClick = (i) => gotoStep(i);
|
|
199
|
+
/**
|
|
200
|
+
* Process Completion
|
|
201
|
+
* - Records final step timing.
|
|
202
|
+
* - Submits process completion data.
|
|
203
|
+
* - Navigates back on successful completion.
|
|
204
|
+
*/
|
|
205
|
+
const handleFinish = async () => {
|
|
206
|
+
const final = recordStepTime();
|
|
207
|
+
if (await handleProcessSubmit(final)) props.history?.goBack();
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Start Next Process
|
|
211
|
+
* - Records final timing of the current process.
|
|
212
|
+
* - Submits current process data.
|
|
213
|
+
* - Loads and initializes the next linked process.
|
|
214
|
+
*/
|
|
215
|
+
const handleStartNextProcess = async () => {
|
|
216
|
+
const final = recordStepTime();
|
|
217
|
+
if (await handleProcessSubmit(final)) {
|
|
218
|
+
await loadProcess(nextProcessId.next_process_id);
|
|
219
|
+
setCurrentProcessId(nextProcessId.next_process_id);
|
|
220
|
+
setActiveStep(0);
|
|
221
|
+
setShowExternalWindow(true);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Dynamic Step Renderer
|
|
226
|
+
* - Resolves and renders step-specific components dynamically.
|
|
227
|
+
* - Passes configuration, parameters, and handlers to the component.
|
|
228
|
+
* - Handles missing steps or components gracefully.
|
|
229
|
+
*/
|
|
230
|
+
const DynamicComponent = () => {
|
|
231
|
+
const step = steps[activeStep];
|
|
232
|
+
if (!step) return <Empty description="No step selected" />;
|
|
233
|
+
|
|
234
|
+
const Component = allComponents[step.related_page];
|
|
235
|
+
if (!Component) return <Empty description={`Component "${step.related_page}" not found`} />;
|
|
236
|
+
|
|
237
|
+
return <Component {...step.config} {...props} step={step} params={urlParams} onStepComplete={() => setIsStepCompleted(true)} />;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Keyboard Navigation
|
|
241
|
+
* - Enables left and right arrow keys for step navigation.
|
|
242
|
+
* - Prevents navigation beyond step boundaries.
|
|
243
|
+
* - Cleans up event listeners on unmount.
|
|
244
|
+
*/
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
const handleKeyDown = (event) => {
|
|
247
|
+
// Handle Left Arrow key press to go to the previous step
|
|
248
|
+
if (event.key === 'ArrowLeft') {
|
|
249
|
+
if (activeStep > 0) {
|
|
250
|
+
handlePrevious();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Handle Right Arrow key press to go to the next step
|
|
254
|
+
else if (event.key === 'ArrowRight') {
|
|
255
|
+
if (activeStep < steps.length - 1) {
|
|
256
|
+
handleNext();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
262
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
263
|
+
}, [activeStep, steps, handlePrevious, handleNext]);
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Renders the main process UI including timeline, step details,
|
|
267
|
+
* and action buttons. This content is reused in both normal view
|
|
268
|
+
* and external window view.
|
|
269
|
+
*/
|
|
270
|
+
const renderContent = () => (
|
|
271
|
+
<Card>
|
|
272
|
+
<Row gutter={20}>
|
|
273
|
+
<Col xs={24} sm={24} lg={timelineCollapsed ? 2 : 6}>
|
|
274
|
+
<TimelinePanel
|
|
275
|
+
loading={loading}
|
|
276
|
+
steps={steps}
|
|
277
|
+
activeStep={activeStep}
|
|
278
|
+
timelineCollapsed={timelineCollapsed}
|
|
279
|
+
handleTimelineClick={handleTimelineClick}
|
|
280
|
+
setTimelineCollapsed={setTimelineCollapsed}
|
|
281
|
+
/>
|
|
282
|
+
</Col>
|
|
283
|
+
|
|
284
|
+
<Col xs={24} sm={24} lg={timelineCollapsed ? 21 : 18}>
|
|
285
|
+
<div style={{ marginBottom: 20 }}>
|
|
286
|
+
<h2 style={{ margin: 0, fontSize: 16, fontWeight: 600 }}>{steps[activeStep]?.step_name}</h2>
|
|
287
|
+
<p style={{ margin: 0, color: '#666' }}>{steps[activeStep]?.step_description}</p>
|
|
288
|
+
</div>
|
|
289
|
+
<ActionButtons
|
|
290
|
+
loading={loading}
|
|
291
|
+
steps={steps}
|
|
292
|
+
activeStep={activeStep}
|
|
293
|
+
isStepCompleted={isStepCompleted}
|
|
294
|
+
renderDynamicComponent={DynamicComponent}
|
|
295
|
+
handlePrevious={handlePrevious}
|
|
296
|
+
handleNext={handleNext}
|
|
297
|
+
handleSkip={handleSkip}
|
|
298
|
+
handleFinish={handleFinish}
|
|
299
|
+
handleStartNextProcess={handleStartNextProcess}
|
|
300
|
+
nextProcessId={nextProcessId}
|
|
301
|
+
timelineCollapsed={timelineCollapsed}
|
|
302
|
+
/>
|
|
303
|
+
</Col>
|
|
304
|
+
</Row>
|
|
305
|
+
</Card>
|
|
306
|
+
);
|
|
307
|
+
/**
|
|
308
|
+
* Renders content in both the main window and an external window
|
|
309
|
+
* when external window mode is enabled.
|
|
310
|
+
*/
|
|
311
|
+
if (showExternalWindow && props.showExternalWindow) {
|
|
312
|
+
return (
|
|
313
|
+
<>
|
|
314
|
+
<ExternalWindow
|
|
315
|
+
title={steps[activeStep]?.step_name || 'Process Step'}
|
|
316
|
+
onClose={() => setShowExternalWindow(false)}
|
|
317
|
+
// left={window.screenX + window.outerWidth}
|
|
318
|
+
// top={window.screenY}
|
|
319
|
+
width={props.ExternalWindowWidth || 1000}
|
|
320
|
+
height={props.ExternalWindowHeight || 1000}
|
|
321
|
+
>
|
|
322
|
+
{renderContent()}
|
|
323
|
+
</ExternalWindow>
|
|
324
|
+
{renderContent()}
|
|
325
|
+
</>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Default render when external window mode is disabled.
|
|
330
|
+
*/
|
|
331
|
+
return renderContent();
|
|
332
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
.timeline-card .ant-card-body {
|
|
2
|
+
padding: 20px;
|
|
3
|
+
min-height: 400px;
|
|
4
|
+
position: fixed; /* For positioning the arrow */
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.timeline-sidebar {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: 20px;
|
|
11
|
+
transition: all 0.3s ease;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.timeline-step {
|
|
15
|
+
display: flex;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.timeline-step.active .step-number {
|
|
20
|
+
background: #3b82f6;
|
|
21
|
+
color: white;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.timeline-step.completed .step-number {
|
|
25
|
+
background: #22c55e;
|
|
26
|
+
color: white;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.step-marker {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
align-items: center;
|
|
33
|
+
margin-right: 14px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.step-number {
|
|
37
|
+
width: 28px;
|
|
38
|
+
height: 28px;
|
|
39
|
+
border-radius: 50%;
|
|
40
|
+
background: #d9d9d9;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.vertical-line {
|
|
48
|
+
width: 2px;
|
|
49
|
+
height: 40px;
|
|
50
|
+
background: #d9d9d9;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.step-info {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.step-title {
|
|
60
|
+
font-size: 13px;
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.step-main {
|
|
65
|
+
font-size: 15px;
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.toggle-arrow {
|
|
70
|
+
position: absolute;
|
|
71
|
+
top: 50%;
|
|
72
|
+
right: -12px; /* Position it just outside the card body padding */
|
|
73
|
+
transform: translateY(-50%);
|
|
74
|
+
width: 24px;
|
|
75
|
+
height: 24px;
|
|
76
|
+
background: #fff;
|
|
77
|
+
border: 1px solid #f0f0f0;
|
|
78
|
+
border-radius: 50%;
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
|
84
|
+
z-index: 10;
|
|
85
|
+
transition: all 0.3s ease;
|
|
86
|
+
|
|
87
|
+
&:hover {
|
|
88
|
+
background: #f5f5f5;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ============================
|
|
93
|
+
MOBILE & TABLET VIEW FIXES
|
|
94
|
+
============================ */
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@media (max-width: 992px) { // iPad & tablets
|
|
99
|
+
.timeline-card .ant-card-body {
|
|
100
|
+
padding: 12px;
|
|
101
|
+
min-height: auto;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.timeline-sidebar {
|
|
105
|
+
flex-direction: row !important;
|
|
106
|
+
overflow-x: auto;
|
|
107
|
+
gap: 12px;
|
|
108
|
+
padding-bottom: 10px;
|
|
109
|
+
border-bottom: 1px solid #eee;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.timeline-step {
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
align-items: center;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.step-marker {
|
|
118
|
+
margin-right: 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.step-info {
|
|
122
|
+
text-align: center;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.toggle-arrow {
|
|
126
|
+
display: none !important; /* Hide collapse icon */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@media (max-width: 768px) { // mobile screens
|
|
131
|
+
.timeline-sidebar {
|
|
132
|
+
gap: 8px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.step-number {
|
|
136
|
+
width: 24px;
|
|
137
|
+
height: 24px;
|
|
138
|
+
font-size: 12px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.step-title {
|
|
142
|
+
font-size: 11px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.step-main {
|
|
146
|
+
font-size: 13px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.vertical-line {
|
|
150
|
+
display: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Card layout full width */
|
|
154
|
+
.timeline-card .ant-card-body {
|
|
155
|
+
padding: 10px;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|