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.
@@ -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
 
@@ -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
@@ -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
+