ui-soxo-bootstrap-core 2.6.1-dev.3 → 2.6.1-dev.31

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 (68) hide show
  1. package/core/components/extra-info/extra-info-details.js +2 -2
  2. package/core/components/index.js +2 -11
  3. package/core/components/landing-api/landing-api.js +216 -18
  4. package/core/components/landing-api/landing-api.scss +22 -0
  5. package/core/components/license-management/license-alert.js +97 -0
  6. package/core/lib/Store.js +8 -4
  7. package/core/lib/components/global-header/global-header.js +217 -242
  8. package/core/lib/components/index.js +2 -2
  9. package/core/lib/components/sidemenu/sidemenu.js +19 -13
  10. package/core/lib/components/sidemenu/sidemenu.scss +1 -1
  11. package/core/lib/elements/basic/country-phone-input/country-phone-input.js +14 -9
  12. package/core/lib/elements/basic/dragabble-wrapper/draggable-wrapper.js +1 -1
  13. package/core/lib/elements/basic/menu-tree/menu-tree.js +26 -13
  14. package/core/lib/models/forms/components/form-creator/form-creator.js +525 -468
  15. package/core/lib/models/forms/components/form-creator/form-creator.scss +30 -26
  16. package/core/lib/models/menus/components/menu-list/menu-list.js +424 -467
  17. package/core/lib/models/process/components/process-dashboard/process-dashboard.js +469 -3
  18. package/core/lib/models/process/components/process-dashboard/process-dashboard.scss +4 -0
  19. package/core/lib/modules/generic/generic-list/ExportReactCSV.js +28 -2
  20. package/core/lib/pages/change-password/change-password.js +17 -24
  21. package/core/lib/pages/change-password/change-password.scss +45 -48
  22. package/core/lib/pages/login/commnication-mode-selection.js +2 -2
  23. package/core/lib/pages/login/login.js +53 -64
  24. package/core/lib/pages/login/login.scss +9 -0
  25. package/core/lib/pages/login/reset-password.js +17 -17
  26. package/core/lib/pages/login/reset-password.scss +10 -1
  27. package/core/lib/pages/profile/themes.json +4 -4
  28. package/core/lib/utils/api/api.utils.js +53 -45
  29. package/core/lib/utils/common/common.utils.js +49 -35
  30. package/core/lib/utils/generic/generic.utils.js +2 -1
  31. package/core/lib/utils/http/http.utils.js +33 -4
  32. package/core/lib/utils/index.js +4 -1
  33. package/core/models/base/base.js +7 -3
  34. package/core/models/core-scripts/core-scripts.js +147 -126
  35. package/core/models/doctor/components/doctor-add/doctor-add.js +9 -4
  36. package/core/models/menus/components/menu-add/menu-add.js +1 -1
  37. package/core/models/menus/components/menu-lists/menu-lists.js +53 -54
  38. package/core/models/menus/menus.js +49 -2
  39. package/core/models/roles/components/role-add/role-add.js +92 -59
  40. package/core/models/roles/components/role-list/role-list.js +1 -1
  41. package/core/models/staff/components/staff-add/staff-add.js +20 -32
  42. package/core/models/users/components/assign-role/assign-role.js +145 -50
  43. package/core/models/users/components/assign-role/assign-role.scss +209 -45
  44. package/core/models/users/components/assign-role/avatar-props.js +45 -0
  45. package/core/models/users/components/user-add/user-add.js +46 -55
  46. package/core/models/users/components/user-add/user-edit.js +25 -4
  47. package/core/models/users/users.js +9 -1
  48. package/core/modules/dashboard/components/dashboard-card/menu-dashboard-card.js +1 -1
  49. package/core/modules/reporting/components/reporting-dashboard/README.md +316 -0
  50. package/core/modules/reporting/components/reporting-dashboard/adavance-search/advance-search.js +174 -0
  51. package/core/modules/reporting/components/reporting-dashboard/adavance-search/advance-search.scss +76 -0
  52. package/core/modules/reporting/components/reporting-dashboard/display-columns/build-display-columns.js +90 -0
  53. package/core/modules/reporting/components/reporting-dashboard/display-columns/build-display-columns.test.js +74 -0
  54. package/core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.js +448 -0
  55. package/core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.test.js +199 -0
  56. package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +195 -822
  57. package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.scss +43 -0
  58. package/core/modules/reporting/components/reporting-dashboard/reporting-table.js +517 -0
  59. package/core/modules/steps/action-buttons.js +30 -16
  60. package/core/modules/steps/action-buttons.scss +55 -9
  61. package/core/modules/steps/chat-assistant.js +141 -0
  62. package/core/modules/steps/openai-realtime.js +275 -0
  63. package/core/modules/steps/readme.md +167 -0
  64. package/core/modules/steps/steps.js +1286 -60
  65. package/core/modules/steps/steps.scss +703 -86
  66. package/core/modules/steps/timeline.js +21 -19
  67. package/core/modules/steps/voice-navigation.js +709 -0
  68. package/package.json +2 -1
@@ -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
 
@@ -30,7 +30,7 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
30
30
  const [selectedBranches, setSelectedBranches] = useState([]);
31
31
 
32
32
  // for default branch
33
- const [defaultBranch, setDefaultBranch] = useState(null);
33
+ // const [defaultBranch, setDefaultBranch] = useState(null);
34
34
  //Need to check this condition
35
35
  const [authentication, setAuthentication] = useState(false);
36
36
 
@@ -95,7 +95,10 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
95
95
  const [body, setBody] = useState(formContent);
96
96
 
97
97
  const [isPasswordVisible, setIsPasswordVisible] = useState(false);
98
- const [visible, setVisible] = useState(false);
98
+
99
+ // state for doctor and staff visibility
100
+ const [doctorVisible, setDoctorVisible] = useState(false);
101
+ const [staffVisible, setStaffVisible] = useState(false);
99
102
 
100
103
  const formData = {
101
104
  ...body,
@@ -270,8 +273,6 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
270
273
  const getStaff = () => {
271
274
  UsersAPI.getAllStaff()
272
275
  .then((res) => {
273
- console.log('Staff List Response:', res);
274
-
275
276
  if (Array.isArray(res)) {
276
277
  const list = res.map((staff) => ({
277
278
  label: `${staff.shortName || 'No Name'} (${staff.id})`,
@@ -325,20 +326,25 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
325
326
  }
326
327
  if (!formContent) return;
327
328
 
328
- // normalize branch ids to NUMBER
329
329
  const org =
330
330
  typeof formContent.organization_details === 'string' ? JSON.parse(formContent.organization_details) : formContent.organization_details;
331
331
 
332
+ // normalize branch ids to NUMBER
332
333
  const branchIds = (org?.branch_ids || []).map(Number);
333
- const defaultBr = formContent.defaultBranch ? Number(formContent.defaultBranch) : null;
334
+
335
+ // find default branch pointer
336
+ const defaultBranchObj = org?.branch?.find((br) => br.defaultBranch);
337
+
338
+ // extract dbPtr (branch code)
339
+ const defaultBranchCode = defaultBranchObj?.branch_id ? Number(defaultBranchObj.branch_id) : undefined;
334
340
 
335
341
  // state (for filtering)
336
342
  setSelectedBranches(branchIds);
337
343
 
338
- // form (source of truth)
344
+ // form values
339
345
  form.setFieldsValue({
340
346
  selectedBranches: branchIds,
341
- defaultBranch: defaultBr,
347
+ defaultBranch: defaultBranchCode,
342
348
  });
343
349
  }, [formContent, form]);
344
350
  // Generate branch options for Select component
@@ -351,6 +357,10 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
351
357
  * Submit values
352
358
  */
353
359
  const onSubmit = (values) => {
360
+ const mobileData = values.mobile;
361
+
362
+ const mobileWithCountryCode = `+${mobileData.code.dialCode}${mobileData.value}`;
363
+
354
364
  values.defaultBranch = String(values.defaultBranch);
355
365
 
356
366
  /**If PanelOpen is open and edit mode then password will be existing password else new password*/
@@ -372,6 +382,7 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
372
382
  values = {
373
383
  ...values,
374
384
  auth_type: 'LDAP',
385
+ mobile: mobileWithCountryCode,
375
386
  FA: authentication,
376
387
  };
377
388
 
@@ -538,31 +549,17 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
538
549
  borderTop: '1px solid #f0f0f0',
539
550
  color: '#1890ff',
540
551
  }}
541
- onClick={() => setVisible(true)}
552
+ onClick={() => setDoctorVisible(true)}
542
553
  >
543
554
  + Add New Doctor
544
555
  </div>
545
556
  </>
546
557
  )}
547
558
  />
548
- {/* Render DoctorAdd OUTSIDE the Select */}
549
- <DoctorAdd
550
- visible={visible}
551
- onCancel={() => setVisible(false)}
552
- attributes={attributes}
553
- doctorData={selectedDoctor}
554
- doctorId={doctorID}
555
- onSuccess={getDoctors}
556
- />
557
559
  </Form.Item>
558
560
  )}
559
561
  {userType === 'STAFF' && (
560
- <Form.Item
561
- name="staff_code"
562
- preserve={false} // THIS FIXES IT
563
- label="Staff Code"
564
- rules={[{ required: true, message: 'Please select a staff code' }]}
565
- >
562
+ <Form.Item name="staff_code" label="Staff Code" rules={[{ required: true, message: 'Please select a staff code' }]}>
566
563
  <Select
567
564
  placeholder="Select Code"
568
565
  options={staffList}
@@ -581,18 +578,13 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
581
578
  borderTop: '1px solid #f0f0f0',
582
579
  color: '#1890ff',
583
580
  }}
584
- onClick={() => setVisible(true)}
581
+ onClick={() => setStaffVisible(true)}
585
582
  >
586
583
  + Add New Staff
587
584
  </div>
588
585
  </>
589
586
  )}
590
587
  />
591
- {/* Render DoctorAdd OUTSIDE the Select */}
592
-
593
- <StaffAdd visible={visible} onCancel={() => setVisible(false)} staffData={selectedStaff} staffId={staffID} onSuccess={getStaff} />
594
-
595
- <></>
596
588
  </Form.Item>
597
589
  )}
598
590
  </Col>
@@ -610,23 +602,10 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
610
602
  <Form.Item
611
603
  name="mobile"
612
604
  label="Mobile"
613
- rules={[
614
- { required: true, message: 'Please enter your mobile number' },
615
- {
616
- pattern: /^[0-9]{10}$/,
617
- message: 'Mobile number must be exactly 10 digits',
618
- },
619
- ]}
605
+ validateTrigger="onBlur"
606
+ rules={[{ required: true, message: 'Mobile number required' }, { validator: phoneValidator }]}
620
607
  >
621
- <Input
622
- placeholder="Enter Mobile"
623
- maxLength={10}
624
- onKeyPress={(e) => {
625
- if (!/[0-9]/.test(e.key)) {
626
- e.preventDefault();
627
- }
628
- }}
629
- />
608
+ <CountryPhoneInput defaultCountryCode={process.env.REACT_APP_COUNTRYCODE} enableSearch inputStyle={{ width: '100%' }} />
630
609
  </Form.Item>
631
610
  </Col>
632
611
  <Col span={8}>
@@ -669,7 +648,7 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
669
648
  <Col span={8}>
670
649
  {/* Default Branch */}
671
650
  <Form.Item label="Default Branch" name="defaultBranch" rules={[{ required: true, message: 'Please select default branch' }]}>
672
- <Select placeholder="Select Default Branch" onChange={setDefaultBranch}>
651
+ <Select placeholder="Select Default Branch">
673
652
  {branchOptions
674
653
  .filter((opt) => selectedBranches.includes(Number(opt.value)))
675
654
  .map((opt) => (
@@ -682,15 +661,20 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
682
661
  </Col>
683
662
  </Row>
684
663
  <Row gutter={16}>
664
+ <Col span={8}>
665
+ <Form.Item name="user_group" label="User Group" rules={[{ required: true, message: 'Please enter your user group' }]}>
666
+ <Input placeholder="Enter User Group" />
667
+ </Form.Item>
668
+ </Col>
685
669
  <Col span={8}>
686
670
  {formContent?.id ? (
687
671
  <>
688
- <Form.Item>
672
+ {/* <Form.Item>
689
673
  <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
690
674
  <Checkbox onChange={handleCheackChange} />
691
675
  <span>Select the option if you want to change the password</span>
692
676
  </div>
693
- </Form.Item>
677
+ </Form.Item> */}
694
678
 
695
679
  {isPasswordVisible && (
696
680
  <Form.Item
@@ -743,11 +727,7 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
743
727
  {/* Path Ends */}
744
728
 
745
729
  {/* Path */}
746
- <Col span={8}>
747
- <Form.Item name="user_group" label="User Group" rules={[{ required: true, message: 'Please enter your user group' }]}>
748
- <Input placeholder="Enter User Group" />
749
- </Form.Item>
750
- </Col>
730
+
751
731
  {/* <Col span={8}>
752
732
  <Form.Item name="role_id" label="Role" rules={[{ required: true, message: 'Please select a Role' }]}>
753
733
  <Select placeholder="Select Role">
@@ -846,6 +826,17 @@ const UserAdd = ({ model, callback, edit, history, formContent, match, additiona
846
826
  </Form.Item>
847
827
  </Form>
848
828
  )}
829
+ {/* Render DoctorAdd OUTSIDE the Select */}
830
+ <DoctorAdd
831
+ visible={doctorVisible}
832
+ onCancel={() => setDoctorVisible(false)}
833
+ // attributes={attributes}
834
+ doctorData={selectedDoctor}
835
+ doctorId={doctorID}
836
+ onSuccess={getDoctors}
837
+ />
838
+
839
+ <StaffAdd visible={staffVisible} onCancel={() => setStaffVisible(false)} staffData={selectedStaff} staffId={staffID} onSuccess={getStaff} />
849
840
  </section>
850
841
  );
851
842
  };
@@ -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);
@@ -37,25 +38,35 @@ export default function UserEdit(record) {
37
38
  } catch (e) {
38
39
  orgDetails = {};
39
40
  }
41
+
42
+ // find default branch pointer
43
+ const defaultBranchObj = orgDetails?.branch?.find((br) => br.defaultBranch);
44
+
45
+ // extract dbPtr (branch code)
46
+ const defaultBranchCode = defaultBranchObj?.branch_id ? Number(defaultBranchObj.branch_id) : undefined;
47
+
48
+ const formattedMobile = formatPhoneForForm(apiData.mobile);
49
+
40
50
  // Construct mapped data object
41
51
  const mappedData = {
42
52
  id: apiData.id,
43
53
  user_type: otherDetails.user_type || apiData.user_type,
44
54
  name: apiData.name,
45
55
  email: apiData.email,
46
- mobile: apiData.mobile,
56
+ mobile: formattedMobile,
47
57
  designation: apiData.designation_code,
48
- department: apiData.department_id,
58
+ department: Number(apiData.department_id),
49
59
  // Handle selected branches and default branch
50
60
  organization_details: orgDetails,
51
61
  selectedBranches: orgDetails.branch_ids || [],
52
- defaultBranch: apiData.branch_id || null,
62
+ defaultBranch: defaultBranchCode || null,
53
63
  role_id: apiData.role_id,
54
64
  password: apiData.password,
55
65
  user_group: apiData.user_group,
56
66
  // Handle doctor codes
57
67
  default_code: apiData.doctor_code,
58
68
  doctor_code: apiData.doctor_code,
69
+ staff_code: apiData.staff_id,
59
70
  auth_type: apiData.auth_type,
60
71
  FA: apiData.FA,
61
72
  active: apiData.active ? true : false,
@@ -81,7 +92,17 @@ export default function UserEdit(record) {
81
92
  <Skeleton active />
82
93
  </div>
83
94
  ) : (
84
- <UserAdd mode="Edit" formContent={userData} callback={() => setVisible(false)} edit={true} />
95
+ <UserAdd
96
+ mode="Edit"
97
+ formContent={userData}
98
+ callback={() => {
99
+ setVisible(false);
100
+ if (record.callback) {
101
+ record.callback();
102
+ }
103
+ }}
104
+ edit={true}
105
+ />
85
106
  )}
86
107
  </Modal>
87
108
  </div>
@@ -292,7 +292,12 @@ class Users extends Base {
292
292
  information from the API. */
293
293
 
294
294
  getUser = ({ id }) => {
295
- return ApiUtils.get({ url: `users/${id}` });
295
+ return ApiUtils.get({
296
+ url: `users/${id}`,
297
+ headers: {
298
+ db_ptr: 'nuraho',
299
+ },
300
+ });
296
301
  };
297
302
  /* The `getUserRole` method is a function that takes an object with an `id` property as a parameter.
298
303
  It then uses the `ApiUtils.get` function to make a GET request to the endpoint `core-user-roles`
@@ -364,6 +369,9 @@ class Users extends Base {
364
369
  return ApiUtils.post({
365
370
  url: `bookings/trigger-reset-password-link`,
366
371
  formBody,
372
+ headers: {
373
+ db_ptr: 'nuraho',
374
+ },
367
375
  });
368
376
  };
369
377
  }
@@ -207,7 +207,7 @@ export default function MenuDashboardCard({ record, selectedCardId, scope, dbPtr
207
207
 
208
208
  '.statistic-card-container': {
209
209
  '.ant-statistic-title': {
210
- color: 'rgba(0, 0, 0, 0.45)',
210
+ color: 'rgba(0, 0, 0, 0.85)',
211
211
  fontSize: '10px',
212
212
  marginBottom: '0px',
213
213
  },
@@ -0,0 +1,316 @@
1
+ # Reporting Dashboard
2
+
3
+ This module renders configurable reports using:
4
+ - backend-driven `input_parameters` (filter form)
5
+ - backend-driven `display_columns` (table columns)
6
+ - report data from `CoreScripts.getReportingLisitng(...)`
7
+
8
+ Main component:
9
+ - `core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js`
10
+
11
+ Display column internals:
12
+ - `core/modules/reporting/components/reporting-dashboard/display-columns/build-display-columns.js`
13
+ - `core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.js`
14
+
15
+ ## Data Flow Overview
16
+
17
+ 1. Dashboard loads `CoreScripts.getRecord({ id, dbPtr })`.
18
+ 2. It parses:
19
+ - `input_parameters` JSON string -> filter form configuration
20
+ - `display_columns` JSON string -> table column configuration
21
+ 3. It submits filter values to `CoreScripts.getReportingLisitng(coreScriptId, formBody, dbPtr)`.
22
+ 4. It renders the table with configured display columns.
23
+
24
+ If `display_columns` is missing, columns are auto-generated from response keys.
25
+
26
+ ## ReportingDashboard Props
27
+
28
+ Common props:
29
+ - `match` / `reportId`: report id source (`reportId` overrides route id)
30
+ - `dbPtr`: db pointer (falls back to `localStorage.db_ptr`)
31
+ - `CustomComponents`: map of reusable custom components
32
+ - `scope`: optional object to override filter payload body
33
+ - `dashBoardIds`: optional cards for dashboard switching
34
+ - `showScanner`: enables QR scanner button
35
+ - `barcodeFilterKey`: field used to match scanned QR value
36
+ - `isFixedIndex`: fixes index `#` column to left
37
+ - `attributes`: JSON string for extra UI controls (for example `buttonAttributes`)
38
+
39
+ ## Input Parameters Configuration
40
+
41
+ `input_parameters` is stored as a JSON string in backend and parsed at runtime.
42
+
43
+ Example:
44
+
45
+ ```json
46
+ [
47
+ {
48
+ "title": "From Date",
49
+ "field": "start",
50
+ "type": "date",
51
+ "default": "startOfDay",
52
+ "required": true
53
+ },
54
+ {
55
+ "title": "To Date",
56
+ "field": "end",
57
+ "type": "date",
58
+ "default": "endOfDay",
59
+ "required": true
60
+ },
61
+ {
62
+ "title": "Department",
63
+ "field": "dept_id",
64
+ "type": "reference-select",
65
+ "modelName": "Department",
66
+ "required": false
67
+ },
68
+ {
69
+ "title": "Status",
70
+ "field": "status",
71
+ "type": "select",
72
+ "options": [
73
+ { "label": "Pending", "value": "Pending" },
74
+ { "label": "Completed", "value": "Completed" }
75
+ ]
76
+ }
77
+ ]
78
+ ```
79
+
80
+ ### Supported default date values
81
+
82
+ For `type: "date"`, supported `default` values:
83
+ - `startOfDay`
84
+ - `endOfDay`
85
+ - `startOfWeek`
86
+ - `endOfWeek`
87
+ - `currentDate`
88
+
89
+ If no date default is provided, current date is used.
90
+
91
+ ### URL synchronization
92
+
93
+ - On load, if URL contains `?field=value`, that value is used over defaults.
94
+ - On submit, active input values are written back to URL query params.
95
+ - Date values are URL-formatted as `YYYY-MM-DD`.
96
+
97
+ ### Hiding input parameter form
98
+
99
+ If `other_details1` JSON includes:
100
+
101
+ ```json
102
+ { "isDisableInputParameters": true }
103
+ ```
104
+
105
+ the input form is hidden, but report data still loads.
106
+
107
+ ## Display Columns Configuration
108
+
109
+ `display_columns` is also a backend JSON string.
110
+
111
+ Each entry maps to one table column.
112
+
113
+ Basic example:
114
+
115
+ ```json
116
+ [
117
+ { "title": "OP No", "field": "opno", "isSortingEnabled": true },
118
+ { "title": "Guest Name", "field": "guest_name", "isFilterEnabled": true },
119
+ { "title": "Amount", "field": "amount", "type": "number", "width": 120 }
120
+ ]
121
+ ```
122
+
123
+ ### Common column keys
124
+
125
+ - `title`: header text
126
+ - `tooltip`: optional header tooltip override
127
+ - `field`: record key to display
128
+ - `width`: numeric width (default `160`)
129
+ - `isFixedColumn`: AntD fixed position (`"left"` / `"right"`)
130
+ - `isFilterEnabled`: generates unique-value filters from table data
131
+ - `isSortingEnabled`: enables string sort on `field`
132
+ - `color`: default text color
133
+ - `type: "number"`: right-aligns cell
134
+
135
+ ## Action Columns (Backward-Compatible Rules)
136
+
137
+ Action logic is intentionally layered to avoid breaking existing reports.
138
+
139
+ Compatibility rules:
140
+ 1. `entry.field === "action"` -> **legacy action** behavior
141
+ 2. else if `entry.type === "action"` -> **new action layer**
142
+ 3. all other types use existing non-action behavior
143
+
144
+ ### Legacy action example (existing reports)
145
+
146
+ ```json
147
+ {
148
+ "title": "Action",
149
+ "field": "action",
150
+ "redirect_link": "/process/@opb_id;",
151
+ "replace_variables": [{ "field": "opb_id" }],
152
+ "display_name_link": "View"
153
+ }
154
+ ```
155
+
156
+ Legacy label behavior:
157
+ - `display_name_link` or `"View"`
158
+
159
+ ### New action layer example (dynamic API label)
160
+
161
+ ```json
162
+ {
163
+ "title": "Action",
164
+ "type": "action",
165
+ "field": "action_text",
166
+ "redirect_link": "/process/@opb_id;",
167
+ "replace_variables": [{ "field": "opb_id" }],
168
+ "label": "View"
169
+ }
170
+ ```
171
+
172
+ New action label fallback order:
173
+ 1. `record[entry.field]` (example: `record.action_text`)
174
+ 2. `entry.label`
175
+ 3. `entry.display_name_link`
176
+ 4. `"View"`
177
+
178
+ ## Other Display Types
179
+
180
+ ### External link column
181
+
182
+ ```json
183
+ { "title": "Report", "field": "report_url", "type": "link" }
184
+ ```
185
+
186
+ Renders anchor with `View` label if URL exists.
187
+
188
+ ### Color tag or span
189
+
190
+ ```json
191
+ { "title": "Status", "field": "status", "enableColor": true, "columnType": "tag" }
192
+ ```
193
+
194
+ or
195
+
196
+ ```json
197
+ { "title": "Status", "field": "status", "enableColor": true, "columnType": "span" }
198
+ ```
199
+
200
+ Uses `record.color_code`.
201
+
202
+ ### Dynamic icon mapping
203
+
204
+ ```json
205
+ {
206
+ "title": "State",
207
+ "field": "state",
208
+ "displayIcons": {
209
+ "Pending": { "icon": "ClockCircleOutlined", "color": "orange", "size": "14px" },
210
+ "Done": { "icon": "CheckCircleOutlined", "color": "green", "size": "14px" }
211
+ }
212
+ }
213
+ ```
214
+
215
+ Icon names are taken from `@ant-design/icons`.
216
+
217
+ ### Custom component column
218
+
219
+ ```json
220
+ {
221
+ "title": "Info",
222
+ "field": "custom",
223
+ "component": "ResultEntryInfo",
224
+ "props": [
225
+ { "field": "description_text", "value": "description" },
226
+ { "field": "opb_id", "value": "opbId" }
227
+ ],
228
+ "config": { "allowEdit": true }
229
+ }
230
+ ```
231
+
232
+ How it resolves:
233
+ - `component` must exist in merged `CustomComponents`
234
+ - each `props` item maps `record[field]` to prop key `value`
235
+ - `config` is spread as static props
236
+ - `callback` prop triggers dashboard `refresh()`
237
+
238
+ ## Redirect Template Variables
239
+
240
+ Action redirects support template replacement in `redirect_link`:
241
+ - placeholder format: `@field;`
242
+ - replacements defined in `replace_variables`
243
+
244
+ Example:
245
+
246
+ ```json
247
+ {
248
+ "redirect_link": "/lab/@opb_id;/visit/@opno;",
249
+ "replace_variables": [{ "field": "opb_id" }, { "field": "opno" }]
250
+ }
251
+ ```
252
+
253
+ With row `{ "opb_id": 12, "opno": "OP-55" }`:
254
+ - result -> `/lab/12/visit/OP-55`
255
+
256
+ ## QR Scanner Behavior
257
+
258
+ When scanner is enabled:
259
+ - scanned value is matched against `patient[barcodeFilterKey]`
260
+ - redirect uses first configured action column with this priority:
261
+ 1. legacy action column (`field === "action"`)
262
+ 2. new action column (`type === "action"`)
263
+
264
+ ## Export Behavior
265
+
266
+ - Export uses current table columns.
267
+ - For `field === "custom"`, export attempts to read the prop mapping where `value === "description"` and exports that source field.
268
+ - Other columns export `record[field]`.
269
+
270
+ ## End-to-End Example
271
+
272
+ `display_columns`:
273
+
274
+ ```json
275
+ [
276
+ { "title": "OP No", "field": "opno", "isSortingEnabled": true, "width": 140 },
277
+ { "title": "Guest", "field": "guest_name", "isFilterEnabled": true, "width": 220 },
278
+ {
279
+ "title": "Status",
280
+ "field": "status",
281
+ "enableColor": true,
282
+ "columnType": "tag"
283
+ },
284
+ {
285
+ "title": "Action",
286
+ "type": "action",
287
+ "field": "action_text",
288
+ "redirect_link": "/process-detail/@opb_id;",
289
+ "replace_variables": [{ "field": "opb_id" }],
290
+ "label": "View"
291
+ }
292
+ ]
293
+ ```
294
+
295
+ Sample API row:
296
+
297
+ ```json
298
+ {
299
+ "opb_id": 1001,
300
+ "opno": "OP-1001",
301
+ "guest_name": "Aisha Rahman",
302
+ "status": "Pending",
303
+ "color_code": "orange",
304
+ "action_text": "Start Consultation"
305
+ }
306
+ ```
307
+
308
+ Rendered action label:
309
+ - `Start Consultation` (dynamic value from API)
310
+
311
+ ## Notes for Developers
312
+
313
+ - Keep `display_columns` as JSON string in DB for backward compatibility.
314
+ - Prefer `type: "action"` for new dynamic action labels.
315
+ - Reserve `field: "action"` for legacy behavior.
316
+ - If adding new display types, implement in `display-cell-renderer.js` and keep order explicit.