ui-soxo-bootstrap-core 2.6.46-dev.5 → 2.6.46-dev.6

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.
@@ -2,7 +2,7 @@ import { useContext, useEffect, useRef, useState } from 'react';
2
2
 
3
3
  import { Route, Switch } from 'react-router-dom';
4
4
 
5
- import { Skeleton } from 'antd';
5
+ import { Skeleton, message, Modal } from 'antd';
6
6
 
7
7
  import { Card, ChangePassword, GlobalContext, GlobalHeader, ModuleRoutes, Profile, SettingsUtil, SpotlightSearch, useTranslation } from '../../lib';
8
8
 
@@ -46,7 +46,6 @@ function getRandomMessage(previousMessage = '') {
46
46
  */
47
47
  export default function LandingApi({ history, CustomComponents, CustomModels, appSettings, transitionPending = false, onHomeReady, ...props }) {
48
48
  const [loader, setLoader] = useState(false);
49
-
50
49
  // const [modules, setModules] = useState([]);
51
50
 
52
51
  const [connected] = useState();
@@ -66,7 +65,67 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
66
65
 
67
66
  var config = {};
68
67
 
69
- //fetch license summary
68
+
69
+
70
+ // Variable decides the control of homepage
71
+ // #TODO This is a temporary fix - Homemage
72
+
73
+ let disableHomepage = false;
74
+
75
+ if (process.env.REACT_APP_DISABLEHOMEPAGE) {
76
+ disableHomepage = JSON.parse(process.env.REACT_APP_DISABLEHOMEPAGE);
77
+ }
78
+
79
+ /**
80
+ * Normalizes the user's branch access list from `organization_details`.
81
+ *
82
+ * The API can return `organization_details` as a JSON string, so we always
83
+ * parse it through `safeJSON` before reading the branch collection.
84
+ *
85
+ * @returns {Array} List of branches the current user can access.
86
+ */
87
+ const getAccessibleBranches = () => {
88
+ const orgDetails = safeJSON(user?.organization_details);
89
+ return Array.isArray(orgDetails?.branch) ? orgDetails.branch : [];
90
+ };
91
+
92
+ /**
93
+ * Resolves the currently selected branch record using the persisted db pointer.
94
+ *
95
+ * @param {Array} branches
96
+ * @returns {Object|null}
97
+ */
98
+ const getCurrentBranchRecord = (branches) => {
99
+ const currentDbPtr = localStorage.getItem('db_ptr');
100
+ return branches.find((branch) => String(branch.dbPtr) === String(currentDbPtr)) || null;
101
+ };
102
+
103
+ /**
104
+ * Resolves the target branch from the URL `index` query parameter.
105
+ *
106
+ * @param {Array} branches
107
+ * @param {string|null} branchId
108
+ * @returns {Object|null}
109
+ */
110
+ const getBranchRecordById = (branches, branchId) => {
111
+ return branches.find((branch) => String(branch.branch_id) === String(branchId)) || null;
112
+ };
113
+
114
+ /**
115
+ * Persists branch-specific auth data after a successful switch.
116
+ *
117
+ * @param {Object} tokenBundle
118
+ * @param {string} dbPtr
119
+ */
120
+ const persistBranchSession = (tokenBundle, dbPtr) => {
121
+ const accessToken = tokenBundle?.access_token;
122
+ const refreshToken = tokenBundle?.refresh_token;
123
+
124
+ if (accessToken) localStorage.setItem('access_token', accessToken);
125
+ if (refreshToken) localStorage.setItem('refresh_token', refreshToken);
126
+ if (dbPtr) localStorage.setItem('db_ptr', dbPtr);
127
+ };
128
+
70
129
  const fetchSummary = async () => {
71
130
  try {
72
131
  const res = await MenusAPI.getSummary();
@@ -81,16 +140,6 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
81
140
  console.error(err);
82
141
  }
83
142
  };
84
-
85
- // Variable decides the control of homepage
86
- // #TODO This is a temporary fix - Homemage
87
-
88
- let disableHomepage = false;
89
-
90
- if (process.env.REACT_APP_DISABLEHOMEPAGE) {
91
- disableHomepage = JSON.parse(process.env.REACT_APP_DISABLEHOMEPAGE);
92
- }
93
-
94
143
  // useEffect(() => {
95
144
 
96
145
  // // Initialize the menus for the logged in user
@@ -106,6 +155,67 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
106
155
  // }
107
156
  // }, [loader]);
108
157
 
158
+ /**
159
+ * Synchronizes the active branch with the `index` query parameter.
160
+ *
161
+ * Flow:
162
+ * 1. Read the target branch id from the URL.
163
+ * 2. Compare it against the branch represented by the current `db_ptr`.
164
+ * 3. Switch branch only when the user has access and the branch actually differs.
165
+ * 4. Refresh auth/profile state and reload menus for the new branch context.
166
+ */
167
+ useEffect(() => {
168
+ const handleUrlBranchSwitch = async () => {
169
+ const searchParams = new URLSearchParams(history.location.search);
170
+ const urlDbPtr = searchParams.get('index');
171
+ if (!urlDbPtr) return;
172
+
173
+ const accessibleBranches = getAccessibleBranches();
174
+ const currentBranch = getCurrentBranchRecord(accessibleBranches);
175
+ const targetBranch = getBranchRecordById(accessibleBranches, urlDbPtr);
176
+
177
+ if (!targetBranch || String(currentBranch?.branch_id) === String(urlDbPtr)) return;
178
+
179
+ setLoader(true);
180
+
181
+ try {
182
+ const switchResult = await MenusAPI.switchBranch(
183
+ {
184
+ firm_id: targetBranch.firm_ptr,
185
+ branch_id: targetBranch.branch_id,
186
+ },
187
+ targetBranch.dbPtr
188
+ );
189
+
190
+ if (!switchResult?.success) {
191
+ Modal.error({
192
+ title: 'Branch Switch Failed',
193
+ content: switchResult?.message || 'An error occurred while attempting to switch branches.',
194
+ });
195
+ return;
196
+ }
197
+
198
+ persistBranchSession(switchResult?.token, targetBranch.dbPtr);
199
+ window.dispatchEvent(new CustomEvent('branchChanged', { detail: targetBranch.dbPtr }));
200
+
201
+ const accessToken = switchResult?.token?.access_token;
202
+ const profileResult = await MenusAPI.getProfile(accessToken);
203
+ const updatedUser = { ...profileResult, loggedCheckDone: true };
204
+
205
+ dispatch({ type: 'user', payload: updatedUser });
206
+ localStorage.setItem('userInfo', JSON.stringify(updatedUser));
207
+
208
+ await initializeUserMenus();
209
+ } catch (error) {
210
+ console.error('Auto branch switch failed:', error);
211
+ } finally {
212
+ setLoader(false);
213
+ }
214
+ };
215
+
216
+ if (user?.id && history?.location) handleUrlBranchSwitch();
217
+ }, [history?.location?.search, user?.id]);
218
+
109
219
  useEffect(() => {
110
220
  // Initialize the menus for the logged in user
111
221
  initializeUserMenus();
@@ -183,7 +293,7 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
183
293
  setLoader(true);
184
294
 
185
295
  // setReports(report)
186
-
296
+ fetchSummary();
187
297
  const result = await MenusAPI.getMenus(user);
188
298
 
189
299
  // console.log(result);
@@ -109,7 +109,7 @@ import ConsentComponent from './consent/consent'
109
109
  import TaskOverviewLegacy from './../models/process/components/task-overview-legacy/task-overview-legacy'
110
110
 
111
111
  import ReportingDashboard from '../../modules/reporting/components/reporting-dashboard/reporting-dashboard';
112
-
112
+ import ReportingTable from '../../modules/reporting/components/reporting-dashboard/reporting-table'
113
113
  import ProcessStepsPage from '../../modules/steps/steps'
114
114
  export {
115
115
 
@@ -198,7 +198,7 @@ export {
198
198
  TaskOverviewLegacy,
199
199
  // WebCamera,
200
200
  ConsentComponent,
201
-
201
+ ReportingTable,
202
202
  ReportingDashboard,
203
203
  ProcessStepsPage
204
204
 
@@ -69,7 +69,6 @@ class CoreScript extends Base {
69
69
  // Settings db pointer
70
70
  if (!dbPtr) dbPtr = localStorage.db_ptr;
71
71
  return ApiUtils.post({
72
- // baseUrl: 'http://localhost:8002/dev/',
73
72
  url: `core-scripts/dashboardquery/${id}`,
74
73
  formBody,
75
74
  headers: {
@@ -116,6 +115,20 @@ class CoreScript extends Base {
116
115
  });
117
116
  };
118
117
 
118
+ getCorescript = (formBody,dbPtr) => {
119
+
120
+ if (!dbPtr) dbPtr = localStorage.db_ptr;
121
+ return ApiUtils.post({
122
+ // baseUrl: 'http://localhost:8002/dev/',
123
+ url: `core-scripts/get-core-script`,
124
+ headers: {
125
+ 'Content-Type': 'application/json',
126
+ Authorization: 'Bearer ' + localStorage.access_token,
127
+ db_ptr: dbPtr,
128
+ },
129
+ formBody,
130
+ });
131
+ };
119
132
  /**
120
133
  *
121
134
  */
@@ -160,6 +160,29 @@ class MenusAPI extends Base {
160
160
  });
161
161
  };
162
162
 
163
+ getBranches = () => {
164
+ return ApiUtils.get({
165
+ url: 'branches',
166
+ });
167
+ };
168
+
169
+ switchBranch = (formBody, dbPtr) => {
170
+ return ApiUtils.post({
171
+ url: `auth/switch-branch`,
172
+ headers: { db_ptr: dbPtr },
173
+ formBody,
174
+ });
175
+ };
176
+
177
+ getProfile = (token) => {
178
+ return ApiUtils.get({
179
+ url: 'auth/profile',
180
+ headers: {
181
+ Authorization: `Bearer ${token}`,
182
+ },
183
+ });
184
+ };
185
+
163
186
  /**
164
187
  * create menu
165
188
  */
@@ -185,7 +208,6 @@ class MenusAPI extends Base {
185
208
  : 'menus/get-menus'; // NURA
186
209
 
187
210
  if (!dbPtr) dbPtr = localStorage.db_ptr;
188
-
189
211
  return this.get({
190
212
  url,
191
213
  config,
@@ -1,7 +1,10 @@
1
- import React from 'react';
2
- import { Tag } from 'antd';
1
+ import React, { useState, useContext } from 'react';
2
+ import { Modal, Tag } from 'antd';
3
3
  import * as Icons from '@ant-design/icons';
4
4
  import { Link } from 'react-router-dom';
5
+ import { GlobalContext, safeJSON, Location } from '../../../../../lib';
6
+ // import { PdfViewer } from '../../../../../lib';
7
+ import { CoreScripts } from '../../../../../models';
5
8
 
6
9
  /**
7
10
  * Utilities for rendering Reporting Dashboard display columns.
@@ -74,7 +77,7 @@ export function isActionTypeEntry(entry = {}) {
74
77
  * @param {DisplayRecord} record
75
78
  * @returns {string}
76
79
  */
77
- export function getRedirectLink(entry = {}, record = {}) {
80
+ export function getRedirectLink(entry = {}, record = {}, CustomComponents = {}) {
78
81
  let redirectLink = entry.redirect_link || '';
79
82
 
80
83
  if (Array.isArray(entry.replace_variables)) {
@@ -87,6 +90,41 @@ export function getRedirectLink(entry = {}, record = {}) {
87
90
  return redirectLink;
88
91
  }
89
92
 
93
+ /**
94
+ * Resolves the PDF/file location for file-based action entries.
95
+ *
96
+ * Supports either:
97
+ * - `entry.file_location` as a direct URL/path
98
+ * - `entry.file_location` as a record field name
99
+ *
100
+ * @param {DisplayColumnEntry} entry
101
+ * @param {DisplayRecord} record
102
+ * @returns {string}
103
+ */
104
+ export function getFileLocation(entry = {}, record = {}) {
105
+ if (entry.file_location && typeof entry.file_location === 'string' && record[entry.file_location]) {
106
+ return record[entry.file_location];
107
+ }
108
+
109
+ return record.file_location || entry.file_location || '';
110
+ }
111
+
112
+ /**
113
+ * Resolves a value from the row when a config field points to a record key,
114
+ * otherwise returns the provided literal value.
115
+ *
116
+ * @param {*} value
117
+ * @param {DisplayRecord} record
118
+ * @returns {*}
119
+ */
120
+ function resolveRecordValue(value, record = {}) {
121
+ if (typeof value === 'string' && Object.prototype.hasOwnProperty.call(record, value)) {
122
+ return record[value];
123
+ }
124
+
125
+ return value;
126
+ }
127
+
90
128
  /**
91
129
  * Resolves action label text with backward-compatible precedence.
92
130
  *
@@ -170,6 +208,153 @@ function renderCustomComponent({ entry, record, CustomComponents, refresh }) {
170
208
  );
171
209
  }
172
210
 
211
+ /**
212
+ * Renders a PDF file action inside a modal viewer.
213
+ *
214
+ * @param {Object} root0
215
+ * @param {DisplayColumnEntry} root0.entry
216
+ * @param {DisplayRecord} root0.record
217
+ * @param {Object.<string, React.ComponentType<any>>} root0.CustomComponents
218
+ * @returns {React.ReactNode}
219
+ */
220
+ function FileActionLink({ entry, record, CustomComponents }) {
221
+ const [isPdfVisible, setIsPdfVisible] = useState(false);
222
+ const fileUrl = getFileLocation(entry, record);
223
+ const actionLabel = getActionLabel(entry, record);
224
+ const FileLoaderComponent = CustomComponents?.FileLoader;
225
+
226
+ if (!fileUrl) {
227
+ return null;
228
+ }
229
+
230
+ const fileLoaderProps = {
231
+ url: fileUrl,
232
+ type: resolveRecordValue(entry.file_type, record) || 'pdf',
233
+ defaultScale: resolveRecordValue(entry.default_scale, record),
234
+ viewerType: resolveRecordValue(entry.viewer_type, record),
235
+ config: {
236
+ requireLinuxPath: resolveRecordValue(entry.require_linux_path, record),
237
+ replaceBranch: resolveRecordValue(entry.replace_branch, record),
238
+ ...(entry.file_loader_config || {}),
239
+ },
240
+ entry,
241
+ record,
242
+ };
243
+
244
+ return (
245
+ <>
246
+ {/* 1. Add the trigger link/button here */}
247
+ <a
248
+ onClick={(e) => {
249
+ e.preventDefault();
250
+ setIsPdfVisible(true);
251
+ }}
252
+ style={{ cursor: 'pointer' }}
253
+ >
254
+ {actionLabel}
255
+ </a>
256
+
257
+ <Modal
258
+ open={isPdfVisible}
259
+ onCancel={() => setIsPdfVisible(false)}
260
+ footer={null}
261
+ destroyOnClose
262
+ width={950}
263
+ style={{ top: 10 }}
264
+ title={actionLabel}
265
+ >
266
+ {/* 2. Ensure the loader component exists */}
267
+ {FileLoaderComponent ? <FileLoaderComponent {...fileLoaderProps} /> : <p>Loader not found</p>}
268
+ </Modal>
269
+ </>
270
+ );
271
+ }
272
+
273
+ /**
274
+ * Returns the branch access list from the signed-in user's organization details.
275
+ *
276
+ * `organization_details` may arrive as a JSON string, so this helper keeps the
277
+ * parsing logic in one place before branch comparisons are made.
278
+ *
279
+ * @param {Object} user
280
+ * @returns {Array}
281
+ */
282
+ function getAccessibleBranches(user = {}) {
283
+ const orgDetails = safeJSON(user?.organization_details);
284
+ return Array.isArray(orgDetails?.branch) ? orgDetails.branch : [];
285
+ }
286
+
287
+ /**
288
+ * Resolves branch metadata used to decide whether an action link should prompt
289
+ * for a branch switch before navigation.
290
+ *
291
+ * When an action entry includes `replace_variables` with the `index` field, the
292
+ * row is treated as branch-aware content. We compare the record's branch id
293
+ * against the active branch from `localStorage.db_ptr`, and also resolve whether
294
+ * the user has access to the target branch.
295
+ *
296
+ * @param {DisplayColumnEntry} entry
297
+ * @param {DisplayRecord} record
298
+ * @param {Object} user
299
+ * @returns {{requiresBranchSwitch: boolean, hasTargetBranchAccess: boolean}}
300
+ */
301
+ function getBranchNavigationState(entry = {}, record = {}, user = {}) {
302
+ const branchFieldConfig = entry.replace_variables?.find((variable) => variable.field === 'index');
303
+
304
+ if (!branchFieldConfig) {
305
+ return { requiresBranchSwitch: false, hasTargetBranchAccess: false };
306
+ }
307
+
308
+ const accessibleBranches = getAccessibleBranches(user);
309
+ const activeDbPtr = localStorage.getItem('db_ptr');
310
+ const activeBranch = accessibleBranches.find((branch) => String(branch.dbPtr) === String(activeDbPtr));
311
+ const targetBranchId = record[branchFieldConfig.field];
312
+ const targetBranch = accessibleBranches.find((branch) => String(branch.branch_id) === String(targetBranchId));
313
+
314
+ return {
315
+ requiresBranchSwitch: Boolean(targetBranchId) && String(activeBranch?.branch_id) !== String(targetBranchId),
316
+ hasTargetBranchAccess: Boolean(targetBranch),
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Renders a branch-switch confirmation link for cross-branch records.
322
+ *
323
+ * The actual branch change is handled by the landing page via the `index`
324
+ * query parameter. This link only confirms intent and blocks navigation when
325
+ * the user does not have access to the target branch.
326
+ *
327
+ * @param {Object} root0
328
+ * @param {string} root0.label
329
+ * @param {string} root0.redirectLink
330
+ * @param {boolean} root0.hasTargetBranchAccess
331
+ * @returns {React.ReactNode}
332
+ */
333
+ function BranchAwareActionLink({ label, redirectLink, hasTargetBranchAccess }) {
334
+ return (
335
+ <a
336
+ onClick={(e) => {
337
+ e.preventDefault();
338
+ Modal.confirm({
339
+ content: 'This data is another branch. Switch to that branch to view details?',
340
+ onOk: () => {
341
+ if (hasTargetBranchAccess) {
342
+ Location.navigate({ url: redirectLink });
343
+ return;
344
+ }
345
+
346
+ Modal.error({
347
+ title: 'Branch Switch Failed',
348
+ content: "You don't have permission to view this branch's details.",
349
+ });
350
+ },
351
+ });
352
+ }}
353
+ >
354
+ {label}
355
+ </a>
356
+ );
357
+ }
173
358
  /**
174
359
  * Renders table cell content for a configured display column.
175
360
  *
@@ -210,10 +395,21 @@ export function renderDisplayCell({ entry, record, CustomComponents, refresh })
210
395
  }
211
396
  return null;
212
397
  }
213
-
214
398
  if (isLegacyActionEntry(entry) || isActionTypeEntry(entry)) {
399
+ if (entry.redirect_link_type === 'file') {
400
+ return <FileActionLink entry={entry} record={record} CustomComponents={CustomComponents} />;
401
+ }
402
+ const { user = {} } = useContext(GlobalContext);
403
+
215
404
  const redirectLink = getRedirectLink(entry, record);
216
- return <Link to={`${redirectLink}`}>{getActionLabel(entry, record)}</Link>;
405
+ const label = getActionLabel(entry, record);
406
+ const { requiresBranchSwitch, hasTargetBranchAccess } = getBranchNavigationState(entry, record, user);
407
+
408
+ if (requiresBranchSwitch) {
409
+ return <BranchAwareActionLink label={label} redirectLink={redirectLink} hasTargetBranchAccess={hasTargetBranchAccess} />;
410
+ }
411
+
412
+ return <Link to={`${redirectLink}`}>{label}</Link>;
217
413
  }
218
414
 
219
415
  if (entry.field === 'custom') {
@@ -3,6 +3,7 @@ import {
3
3
  isLegacyActionEntry,
4
4
  isActionTypeEntry,
5
5
  getRedirectLink,
6
+ getFileLocation,
6
7
  getActionLabel,
7
8
  renderDisplayCell,
8
9
  } from './display-cell-renderer';
@@ -28,6 +29,13 @@ describe('display-cell-renderer', () => {
28
29
  expect(link).toBe('/bill/10/visit/OP100');
29
30
  });
30
31
 
32
+ test('resolves file location from direct config value or record field', () => {
33
+ expect(getFileLocation({ file_location: 'https://example.com/report.pdf' }, {})).toBe('https://example.com/report.pdf');
34
+ expect(getFileLocation({ file_location: 'document_url' }, { document_url: 'https://example.com/record.pdf' })).toBe(
35
+ 'https://example.com/record.pdf',
36
+ );
37
+ });
38
+
31
39
  test('keeps legacy action label behavior unchanged', () => {
32
40
  const entry = {
33
41
  field: 'action',
@@ -77,6 +85,71 @@ describe('display-cell-renderer', () => {
77
85
  expect(element.props.opb_id).toBe(1);
78
86
  });
79
87
 
88
+ test('renders file action entry as pdf viewer trigger', () => {
89
+ const element = renderDisplayCell({
90
+ entry: {
91
+ type: 'action',
92
+ field: 'action_text',
93
+ redirect_link_type: 'file',
94
+ file_location: 'https://example.com/report.pdf',
95
+ label: 'Open PDF',
96
+ },
97
+ record: { action_text: 'Preview PDF' },
98
+ refresh: jest.fn(),
99
+ CustomComponents: {},
100
+ });
101
+
102
+ expect(typeof element.type).toBe('function');
103
+ expect(element.props.entry.redirect_link_type).toBe('file');
104
+ expect(element.props.entry.file_location).toBe('https://example.com/report.pdf');
105
+ });
106
+
107
+ test('passes file action props to CustomComponents.FileUpload when available', () => {
108
+ const FileUpload = () => null;
109
+ const element = renderDisplayCell({
110
+ entry: {
111
+ type: 'action',
112
+ field: 'action_text',
113
+ redirect_link_type: 'file',
114
+ file_location: 'document_path',
115
+ file_type: 'document_type',
116
+ default_scale: 'zoom_level',
117
+ viewer_type: 'inline_viewer',
118
+ require_linux_path: 'needs_linux_path',
119
+ replace_branch: 'should_replace_branch',
120
+ file_loader_config: {
121
+ sample: true,
122
+ },
123
+ },
124
+ record: {
125
+ action_text: 'Preview',
126
+ document_path: '\\\\server\\reports\\doc.pdf',
127
+ document_type: 'pdf',
128
+ zoom_level: 1.25,
129
+ inline_viewer: true,
130
+ needs_linux_path: true,
131
+ should_replace_branch: false,
132
+ },
133
+ refresh: jest.fn(),
134
+ CustomComponents: { FileUpload },
135
+ });
136
+
137
+ expect(typeof element.type).toBe('function');
138
+
139
+ const modalContent = element.props.children[1].props.children;
140
+ expect(modalContent.type).toBe(FileUpload);
141
+ expect(modalContent.props.url).toBe('\\\\server\\reports\\doc.pdf');
142
+ expect(modalContent.props.type).toBe('pdf');
143
+ expect(modalContent.props.defaultScale).toBe(1.25);
144
+ expect(modalContent.props.viewerType).toBe(true);
145
+ expect(modalContent.props.config).toEqual({
146
+ requireLinuxPath: true,
147
+ replaceBranch: false,
148
+ sample: true,
149
+ });
150
+ expect(modalContent.props.record.document_path).toBe('\\\\server\\reports\\doc.pdf');
151
+ });
152
+
80
153
  test('renders styled tag/span/icon/text for non-action path', () => {
81
154
  const tagElement = renderDisplayCell({
82
155
  entry: { field: 'status', enableColor: true, columnType: 'tag' },