ui-soxo-bootstrap-core 2.6.0 → 2.6.1-dev.10
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/.github/workflows/npm-publish.yml +49 -19
- package/core/components/extra-info/extra-info-details.js +2 -2
- package/core/components/menu-template-api/menu-template-api.js +2 -2
- package/core/lib/Store.js +3 -3
- package/core/lib/components/global-header/global-header.js +2 -2
- package/core/lib/components/sidemenu/sidemenu.js +19 -13
- package/core/lib/elements/basic/country-phone-input/country-phone-input.js +35 -60
- package/core/lib/elements/basic/country-phone-input/phone-input.scss +14 -0
- package/core/lib/elements/basic/dragabble-wrapper/draggable-wrapper.js +1 -1
- package/core/lib/elements/basic/menu-tree/menu-tree.js +26 -13
- package/core/lib/models/forms/components/form-creator/form-creator.js +468 -502
- package/core/lib/models/forms/components/form-creator/form-creator.scss +5 -4
- package/core/lib/models/menus/components/menu-list/menu-list.js +424 -467
- package/core/lib/pages/change-password/change-password.js +17 -24
- package/core/lib/pages/change-password/change-password.scss +45 -48
- package/core/lib/pages/login/commnication-mode-selection.js +46 -0
- package/core/lib/pages/login/communication-mode-selection.scss +60 -0
- package/core/lib/pages/login/login.js +153 -24
- package/core/lib/pages/login/login.scss +229 -334
- package/core/lib/pages/login/reset-password.js +124 -0
- package/core/lib/pages/login/reset-password.scss +31 -0
- package/core/lib/pages/profile/themes.json +4 -4
- package/core/lib/utils/api/api.utils.js +71 -48
- package/core/lib/utils/common/common.utils.js +109 -0
- package/core/lib/utils/http/http.utils.js +1 -0
- package/core/lib/utils/index.js +25 -28
- package/core/models/base/base.js +7 -3
- package/core/models/core-scripts/core-scripts.js +9 -0
- package/core/models/doctor/components/doctor-add/doctor-add.js +9 -4
- package/core/models/menus/components/menu-add/menu-add.js +1 -1
- package/core/models/menus/components/menu-lists/menu-lists.js +5 -9
- package/core/models/menus/menus.js +21 -2
- package/core/models/roles/components/role-add/role-add.js +92 -59
- package/core/models/roles/components/role-list/role-list.js +1 -1
- package/core/models/roles/roles.js +9 -0
- package/core/models/staff/components/staff-add/staff-add.js +20 -32
- package/core/models/users/components/assign-role/assign-role.js +145 -50
- package/core/models/users/components/assign-role/assign-role.scss +209 -45
- package/core/models/users/components/assign-role/avatar-props.js +45 -0
- package/core/models/users/components/user-add/user-add.js +47 -56
- package/core/models/users/components/user-add/user-edit.js +25 -4
- package/core/models/users/users.js +34 -8
- package/core/modules/dashboard/components/dashboard-card/menu-dashboard-card.js +1 -1
- package/core/modules/reporting/components/reporting-dashboard/README.md +316 -0
- package/core/modules/reporting/components/reporting-dashboard/adavance-search/advance-search.js +120 -0
- package/core/modules/reporting/components/reporting-dashboard/display-columns/build-display-columns.js +75 -0
- package/core/modules/reporting/components/reporting-dashboard/display-columns/build-display-columns.test.js +74 -0
- package/core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.js +252 -0
- package/core/modules/reporting/components/reporting-dashboard/display-columns/display-cell-renderer.test.js +126 -0
- package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +222 -376
- package/core/modules/steps/action-buttons.js +47 -45
- package/core/modules/steps/action-buttons.scss +35 -6
- package/core/modules/steps/steps.js +12 -10
- package/core/modules/steps/steps.scss +229 -31
- package/core/modules/steps/timeline.js +21 -19
- package/package.json +3 -2
- package/core/components/external-window/DEVELOPER_GUIDE.md +0 -705
|
@@ -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.
|
package/core/modules/reporting/components/reporting-dashboard/adavance-search/advance-search.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
4
|
+
import { Select, Checkbox, Input, Spin } from "antd";
|
|
5
|
+
import { CoreScripts } from "../../../../../models";
|
|
6
|
+
|
|
7
|
+
const { Search } = Input;
|
|
8
|
+
|
|
9
|
+
export default function AdvancedSearchSelect({ field, value = [], onChange }) {
|
|
10
|
+
const [searchResults, setSearchResults] = useState([]); // only API results
|
|
11
|
+
const [selectedItems, setSelectedItems] = useState(value); // persisted selections
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
const debounceRef = useRef(null);
|
|
14
|
+
|
|
15
|
+
// Sync selectedItems if parent value changes externally
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setSelectedItems(value);
|
|
18
|
+
}, [value]);
|
|
19
|
+
|
|
20
|
+
const loadOptions = async (searchText = "") => {
|
|
21
|
+
try {
|
|
22
|
+
setLoading(true);
|
|
23
|
+
|
|
24
|
+
let query = field?.search_query || "";
|
|
25
|
+
query = query.replace("@search_text;", searchText || "");
|
|
26
|
+
|
|
27
|
+
const res = await CoreScripts.getQuery({ script: query });
|
|
28
|
+
const data = Array.isArray(res) ? res[0] : [];
|
|
29
|
+
const apiValues = data?.map((r) => r[field.field]) || [];
|
|
30
|
+
|
|
31
|
+
setSearchResults(apiValues);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error("Search API error", error);
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
loadOptions("");
|
|
41
|
+
}, [field]);
|
|
42
|
+
|
|
43
|
+
const handleSearch = (text) => {
|
|
44
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
45
|
+
debounceRef.current = setTimeout(() => loadOptions(text), 400);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const toggleValue = (checked, item) => {
|
|
49
|
+
let newValues;
|
|
50
|
+
|
|
51
|
+
if (checked) {
|
|
52
|
+
newValues = [...selectedItems, item];
|
|
53
|
+
} else {
|
|
54
|
+
newValues = selectedItems.filter((v) => v !== item);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setSelectedItems(newValues); // update local persisted state immediately
|
|
58
|
+
onChange(newValues);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Merge: selected items always on top, then search results (excluding already selected)
|
|
62
|
+
const displayOptions = [
|
|
63
|
+
...selectedItems,
|
|
64
|
+
...searchResults.filter((item) => !selectedItems.includes(item)),
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Select
|
|
69
|
+
mode="multiple"
|
|
70
|
+
value={value}
|
|
71
|
+
style={{ width: 250 }}
|
|
72
|
+
placeholder={field.caption}
|
|
73
|
+
allowClear
|
|
74
|
+
maxTagCount={0}
|
|
75
|
+
onChange={onChange}
|
|
76
|
+
maxTagPlaceholder={() => (
|
|
77
|
+
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
78
|
+
{field.caption}
|
|
79
|
+
<span
|
|
80
|
+
style={{
|
|
81
|
+
background: "#e6f0ff",
|
|
82
|
+
color: "#1677ff",
|
|
83
|
+
borderRadius: "10px",
|
|
84
|
+
padding: "0 8px",
|
|
85
|
+
fontWeight: 600,
|
|
86
|
+
fontSize: 12,
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{value?.length || 0}
|
|
90
|
+
</span>
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
dropdownRender={() => (
|
|
94
|
+
<div style={{ padding: 10 }}>
|
|
95
|
+
<Search
|
|
96
|
+
placeholder={`Search ${field.caption}`}
|
|
97
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
<div style={{ maxHeight: 200, overflowY: "auto", marginTop: 10 }}>
|
|
101
|
+
{loading ? (
|
|
102
|
+
<Spin />
|
|
103
|
+
) : (
|
|
104
|
+
displayOptions.map((item) => (
|
|
105
|
+
<div key={item} style={{ marginBottom: 6 }}>
|
|
106
|
+
<Checkbox
|
|
107
|
+
checked={selectedItems.includes(item)}
|
|
108
|
+
onChange={(e) => toggleValue(e.target.checked, item)}
|
|
109
|
+
>
|
|
110
|
+
{item}
|
|
111
|
+
</Checkbox>
|
|
112
|
+
</div>
|
|
113
|
+
))
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Tooltip } from 'antd';
|
|
3
|
+
import { renderDisplayCell } from './display-cell-renderer';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves export value definition for a configured display column.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} entry
|
|
9
|
+
* @param {Object} record
|
|
10
|
+
* @returns {*}
|
|
11
|
+
*/
|
|
12
|
+
function getExportDefinition(entry, record) {
|
|
13
|
+
if (entry.field === 'custom') {
|
|
14
|
+
const description = entry.props?.find((p) => p.value === 'description')?.field;
|
|
15
|
+
return description && record[description] ? record[description] : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return record[entry.field];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Builds Ant Design table columns from display column configuration.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} root0
|
|
25
|
+
* @param {Array} root0.columns
|
|
26
|
+
* @param {Array} root0.patients
|
|
27
|
+
* @param {boolean} root0.isFixedIndex
|
|
28
|
+
* @param {Object} root0.CustomComponents
|
|
29
|
+
* @param {Function} root0.refresh
|
|
30
|
+
* @returns {Array}
|
|
31
|
+
*/
|
|
32
|
+
export default function buildDisplayColumns({ columns = [], patients = [], isFixedIndex, CustomComponents, refresh }) {
|
|
33
|
+
const displayColumns = [
|
|
34
|
+
{
|
|
35
|
+
title: '#',
|
|
36
|
+
dataIndex: 'index',
|
|
37
|
+
render: (value, item, index) => index + 1,
|
|
38
|
+
key: 'ColumnIndex',
|
|
39
|
+
fixed: isFixedIndex ? 'left' : null,
|
|
40
|
+
width: 2,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
columns.forEach((entry, index) => {
|
|
45
|
+
displayColumns.push({
|
|
46
|
+
render: (record) =>
|
|
47
|
+
renderDisplayCell({
|
|
48
|
+
entry,
|
|
49
|
+
record,
|
|
50
|
+
CustomComponents,
|
|
51
|
+
refresh,
|
|
52
|
+
}),
|
|
53
|
+
field: entry.field,
|
|
54
|
+
title: (
|
|
55
|
+
<Tooltip title={entry.tooltip || entry.title}>
|
|
56
|
+
<span>{entry.title}</span>
|
|
57
|
+
</Tooltip>
|
|
58
|
+
),
|
|
59
|
+
key: entry.field || `display_column_${index}`,
|
|
60
|
+
width: entry.width ? parseInt(entry.width, 10) : 160,
|
|
61
|
+
fixed: entry.isFixedColumn ? entry.isFixedColumn : null,
|
|
62
|
+
filters:
|
|
63
|
+
entry.isFilterEnabled && Array.isArray(patients)
|
|
64
|
+
? [...new Set(patients.map((item) => item[entry.field]).filter(Boolean))].map((value) => ({ text: value, value }))
|
|
65
|
+
: null,
|
|
66
|
+
onFilter: entry.isFilterEnabled ? (value, record) => record[entry.field] === value : null,
|
|
67
|
+
sorter: entry.isSortingEnabled ? (a, b) => String(a[entry.field]).localeCompare(String(b[entry.field])) : null,
|
|
68
|
+
filterSearch: entry.isFilterEnabled ? entry.isFilterEnabled : false,
|
|
69
|
+
exportDefinition: (record) => getExportDefinition(entry, record),
|
|
70
|
+
align: entry.type === 'number' ? 'right' : 'left',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return displayColumns;
|
|
75
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import buildDisplayColumns from './build-display-columns';
|
|
3
|
+
|
|
4
|
+
describe('build-display-columns', () => {
|
|
5
|
+
test('creates index column plus configured columns', () => {
|
|
6
|
+
const columns = buildDisplayColumns({
|
|
7
|
+
columns: [{ title: 'OP NO', field: 'opno' }],
|
|
8
|
+
patients: [{ opno: 'OP-1' }],
|
|
9
|
+
isFixedIndex: true,
|
|
10
|
+
CustomComponents: {},
|
|
11
|
+
refresh: jest.fn(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(columns.length).toBe(2);
|
|
15
|
+
expect(columns[0].key).toBe('ColumnIndex');
|
|
16
|
+
expect(columns[0].fixed).toBe('left');
|
|
17
|
+
expect(columns[1].field).toBe('opno');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('preserves legacy action behavior and supports new action dynamic label', () => {
|
|
21
|
+
const refresh = jest.fn();
|
|
22
|
+
const columns = buildDisplayColumns({
|
|
23
|
+
columns: [
|
|
24
|
+
{
|
|
25
|
+
title: 'Legacy Action',
|
|
26
|
+
field: 'action',
|
|
27
|
+
redirect_link: '/legacy/@opb_id;',
|
|
28
|
+
replace_variables: [{ field: 'opb_id' }],
|
|
29
|
+
display_name_link: 'Open',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
title: 'New Action',
|
|
33
|
+
type: 'action',
|
|
34
|
+
field: 'action_text',
|
|
35
|
+
redirect_link: '/new/@opb_id;',
|
|
36
|
+
replace_variables: [{ field: 'opb_id' }],
|
|
37
|
+
label: 'Fallback',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
patients: [{ opb_id: 10, action: 'Should not replace legacy label', action_text: 'Review' }],
|
|
41
|
+
isFixedIndex: false,
|
|
42
|
+
CustomComponents: {},
|
|
43
|
+
refresh,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const row = { opb_id: 10, action: 'Should not replace legacy label', action_text: 'Review' };
|
|
47
|
+
|
|
48
|
+
const legacyElement = columns[1].render(row);
|
|
49
|
+
const newLayerElement = columns[2].render(row);
|
|
50
|
+
|
|
51
|
+
expect(legacyElement.props.children).toBe('Open');
|
|
52
|
+
expect(newLayerElement.props.children).toBe('Review');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('custom exportDefinition returns mapped description field value', () => {
|
|
56
|
+
const columns = buildDisplayColumns({
|
|
57
|
+
columns: [
|
|
58
|
+
{
|
|
59
|
+
title: 'Info',
|
|
60
|
+
field: 'custom',
|
|
61
|
+
props: [{ field: 'status_text', value: 'description' }],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
patients: [],
|
|
65
|
+
isFixedIndex: false,
|
|
66
|
+
CustomComponents: {},
|
|
67
|
+
refresh: jest.fn(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const exportValue = columns[1].exportDefinition({ status_text: 'Ready' });
|
|
71
|
+
expect(exportValue).toBe('Ready');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|