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

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.
@@ -0,0 +1,522 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { Table, Skeleton, Input, Modal, message, Pagination } from 'antd';
3
+ import { QrcodeOutlined } from '@ant-design/icons';
4
+ import { ExportReactCSV, getExportData, Card, TableComponent, QrScanner } from './../../../../lib/';
5
+ import moment from 'moment-timezone';
6
+ import { CoreScripts } from './../../../../models/';
7
+ import Button from '../../../../lib/elements/basic/button/button';
8
+ import buildDisplayColumns from './display-columns/build-display-columns';
9
+ import { getRedirectLink } from './display-columns/display-cell-renderer';
10
+ import * as ReportingDashboardComp from '../index';
11
+
12
+ const { Search } = Input;
13
+ const genericComponents = require('./../../../../lib');
14
+
15
+ /**
16
+ * @typedef {Object} ReportingTablePagination
17
+ * @property {number} [current] Current page number.
18
+ * @property {number} [pageSize] Number of rows per page.
19
+ * @property {number} [total] Total number of records available.
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} ReportingTableRequestPayload
24
+ * @property {Object} [body] Request body sent when loading reporting data.
25
+ */
26
+
27
+ /**
28
+ * @typedef {Object} ReportingTableProps
29
+ * @property {Array<Object>} [patients] Preloaded table rows supplied by a parent component.
30
+ * @property {Array<Object>} [columns] Preconfigured display column definitions.
31
+ * @property {boolean} [loading] External loading state used when parent owns data fetching.
32
+ * @property {Object.<string, React.ComponentType<any>>} [CustomComponents] Custom renderers/actions available to display columns.
33
+ * @property {Function} [refresh] Refresh callback passed down to nested action components.
34
+ * @property {boolean} [isFixedIndex] Enables fixed index rendering in generated columns.
35
+ * @property {string} [barcodeFilterKey] Record field compared against scanned QR/barcode values.
36
+ * @property {boolean} [showScanner] Controls whether the QR scanner button is shown.
37
+ * @property {Object} [config] Reporting configuration returned by the core script.
38
+ * @property {ReportingTablePagination} [pagination] External pagination state.
39
+ * @property {(pager: ReportingTablePagination) => void} [handlePagination] Parent-owned pagination handler.
40
+ * @property {string} [attributes] JSON string containing extra button/config attributes.
41
+ * @property {Function} [fetchReportData] Parent refresh callback used in controlled mode.
42
+ * @property {string|number} [reportId] Report id used to load configuration and rows.
43
+ * @property {string|number} [requestId] Alternative report request id used for independent loading.
44
+ * @property {ReportingTableRequestPayload} [requestPayload] Optional request payload override for listing calls.
45
+ * @property {(pager: ReportingTablePagination) => void} [onPaginationChange] Callback fired after independent pagination updates.
46
+ * @property {string} [mode] Report mode used when fetching by mode/submode.
47
+ * @property {string} [submode] Report submode used when fetching by mode/submode.
48
+ * @property {Object} [replacements] Dynamic replacements used by some report APIs and action links.
49
+ * @property {boolean} [isNuradesk] Switches to Nuradesk-specific report loading behavior.
50
+ * @property {string} [dbPtr] Optional branch pointer override for report API calls.
51
+ */
52
+
53
+ /**
54
+ * ReportingTable renders report rows with search, summary, export, QR scan, and
55
+ * pagination support.
56
+ *
57
+ * The component supports two data modes:
58
+ * - controlled mode: parent supplies rows/columns/loading/pagination
59
+ * - independent mode: the component fetches configuration and listing data
60
+ * using `reportId`, `requestId`, or mode/submode inputs
61
+ *
62
+ * @param {ReportingTableProps} props
63
+ * @returns {React.ReactNode}
64
+ */
65
+ export default function ReportingTable({
66
+ patients: propsPatients,
67
+ columns: propsColumns,
68
+ loading: propsLoading,
69
+ CustomComponents,
70
+ refresh,
71
+ isFixedIndex,
72
+ barcodeFilterKey,
73
+ showScanner,
74
+ config: propsConfig,
75
+ pagination: propsPagination,
76
+ handlePagination: propsHandlePagination,
77
+ attributes,
78
+ fetchReportData,
79
+ reportId,
80
+ requestId,
81
+ requestPayload,
82
+ // dbPtr: propsDbPtr,
83
+ onPaginationChange,
84
+ mode,
85
+ submode,
86
+ replacements,
87
+ isNuradesk,
88
+ dbPtr
89
+ }) {
90
+ const [internalPatients, setInternalPatients] = useState([]);
91
+ const [internalColumns, setInternalColumns] = useState([]);
92
+ const [internalLoading, setInternalLoading] = useState(false);
93
+ const [internalConfig, setInternalConfig] = useState({});
94
+ const [internalPagination, setInternalPagination] = useState({ current: 1, pageSize: 20, total: 0 });
95
+
96
+ // Independent mode is enabled when enough identifiers are present for the
97
+ // table to load its own schema and data instead of relying on parent props.
98
+ const shouldFetchData = !!(requestId || reportId || replacements?.submode || replacements?.mode);
99
+ const requestPayloadKey = JSON.stringify(requestPayload || {});
100
+
101
+ const propValues = (attributes && JSON.parse(attributes)) || {};
102
+ const { buttonAttributes = [] } = propValues;
103
+
104
+ const [query, setQuery] = useState('');
105
+ const [exportData, setExportData] = useState({});
106
+ const [isScannerVisible, setScannerVisible] = useState(false);
107
+ const [visible, setVisible] = useState(false);
108
+ const [ActiveComponent, setActiveComponent] = useState(null);
109
+ const [single, setSingle] = useState({});
110
+
111
+ // The table can operate in either controlled or self-fetching mode. These
112
+ // selectors keep the render path mode-agnostic after initial setup.
113
+ const patients = shouldFetchData ? internalPatients : propsPatients;
114
+ const columns = propsColumns?.length ? propsColumns : internalColumns;
115
+ const loading = shouldFetchData ? internalLoading : propsLoading;
116
+ const config = Object.keys(propsConfig || {}).length ? propsConfig : internalConfig;
117
+ const pagination = shouldFetchData ? internalPagination : propsPagination;
118
+ const otherDetails = config?.other_details1 ? JSON.parse(config.other_details1) : {};
119
+ const cols = buildDisplayColumns({
120
+ columns,
121
+ patients,
122
+ isFixedIndex,
123
+ CustomComponents: { ...CustomComponents, ...genericComponents, ...ReportingDashboardComp },
124
+ refresh,
125
+ otherDetails,
126
+ });
127
+
128
+ /**
129
+ * Updates the local text filter used for in-memory search across row values.
130
+ *
131
+ * @param {React.ChangeEvent<HTMLInputElement>} event
132
+ */
133
+ function onSearch(event) {
134
+ setQuery(event.target.value);
135
+ }
136
+
137
+ /**
138
+ * Loads reporting configuration and paginated data when the component is
139
+ * operating in independent mode.
140
+ *
141
+ * Flow:
142
+ * 1. Resolve the active report key and db pointer.
143
+ * 2. Fetch report configuration to build display columns/caption metadata.
144
+ * 3. Fetch listing data using the current page and page size.
145
+ * 4. Sync local pagination and notify the parent if needed.
146
+ *
147
+ * @param {ReportingTablePagination} [pager]
148
+ * @returns {Promise<void>}
149
+ */
150
+ const loadIndependentData = async (pager) => {
151
+ setInternalLoading(true);
152
+ const dbPointer = dbPtr || localStorage.db_ptr;
153
+
154
+ const currentPager = pager || internalPagination;
155
+ let reportKey = requestId || reportId || replacements?.mode;
156
+
157
+ try {
158
+ if (!reportKey) return;
159
+
160
+ // Load the report definition first so display columns and captions stay in
161
+ // sync with the server-side report being requested.
162
+ let formBody = {
163
+ reportId: reportId,
164
+ };
165
+ if (replacements?.mode && replacements?.submode) {
166
+ formBody = {
167
+ mode: replacements.mode,
168
+ submode: replacements.submode,
169
+ // replacements: { ...replacements },
170
+ };
171
+ }
172
+
173
+ let data = await CoreScripts.getCorescript({ ...formBody },dbPointer);
174
+ if (data?.result?.display_columns) {
175
+ setInternalColumns(JSON.parse(data.result.display_columns));
176
+ }
177
+ setInternalConfig(data);
178
+ // if (isNuradesk) {
179
+ // setInternalPatients(data.result || []);
180
+ // setInternalColumns(Object.keys(data.result[0]).map((key) => ({ title: key, field: key })));
181
+ // }
182
+ // }
183
+
184
+ // Then request the actual row data for the active page.
185
+ const baseBody = requestPayload?.body || {};
186
+ let body = {
187
+ body: {
188
+ ...baseBody,
189
+ page: currentPager.current,
190
+ limit: currentPager.pageSize,
191
+ ...(requestPayload?.body ? {} : { mode, submode }),
192
+ },
193
+ };
194
+ // if (!isNuradesk) {
195
+ if (isNuradesk) {
196
+ reportKey = data.result.id;
197
+ body = {
198
+ body: {
199
+ ...replacements,
200
+ page: currentPager.current,
201
+ limit: currentPager.pageSize,
202
+ },
203
+ };
204
+ }
205
+ const result = await CoreScripts.getReportingLisitng(reportKey, body, dbPtr);
206
+ const apiData = Array.isArray(result) ? result : Array.isArray(result?.result) ? result.result : [];
207
+ const resultDetails = apiData[0] || [];
208
+
209
+ setInternalPatients(resultDetails || []);
210
+ // Fall back to inferred columns when the report definition does not
211
+ // provide an explicit `display_columns` config.
212
+ if (!data.result.display_columns && resultDetails.length > 0) {
213
+ setInternalColumns(Object.keys(resultDetails[0]).map((key) => ({ title: key, field: key })));
214
+ }
215
+ // }
216
+
217
+ const nextPagination = {
218
+ current: currentPager.current,
219
+ pageSize: currentPager.pageSize,
220
+ total: resultDetails?.[0]?.TotalCount ?? internalPagination.total,
221
+ };
222
+
223
+ setInternalPagination((prev) => ({
224
+ ...prev,
225
+ ...nextPagination,
226
+ }));
227
+
228
+ if (onPaginationChange) {
229
+ onPaginationChange(nextPagination);
230
+ }
231
+ } catch (error) {
232
+ console.error('Error loading independent table data', error);
233
+ } finally {
234
+ setInternalLoading(false);
235
+ }
236
+ };
237
+
238
+ useEffect(() => {
239
+ if (propsPagination?.current || propsPagination?.pageSize || propsPagination?.total === 0) {
240
+ setInternalPagination((prev) => ({
241
+ ...prev,
242
+ current: propsPagination?.current || prev.current,
243
+ pageSize: propsPagination?.pageSize || prev.pageSize,
244
+ total: typeof propsPagination?.total === 'number' ? propsPagination.total : prev.total,
245
+ }));
246
+ }
247
+ }, [propsPagination?.current, propsPagination?.pageSize, propsPagination?.total]);
248
+
249
+ useEffect(() => {
250
+ if (shouldFetchData) {
251
+ const nextPager = {
252
+ current: propsPagination?.current || 1,
253
+ pageSize: propsPagination?.pageSize || internalPagination.pageSize,
254
+ };
255
+ loadIndependentData(nextPager);
256
+ }
257
+ }, [requestId, reportId, mode, submode, requestPayloadKey]);
258
+
259
+ /**
260
+ * Routes pagination changes to either the internal loader or the parent
261
+ * handler, depending on the current data mode.
262
+ *
263
+ * @param {ReportingTablePagination} pager
264
+ */
265
+ const handlePaginationInternal = (pager) => {
266
+ if (shouldFetchData) {
267
+ loadIndependentData(pager);
268
+ } else if (propsHandlePagination) {
269
+ propsHandlePagination(pager);
270
+ }
271
+ };
272
+
273
+ const filtered = useMemo(() => {
274
+ if (!patients || !query) return patients;
275
+
276
+ return patients.filter((record) => {
277
+ let keys = Object.keys(record);
278
+ let flag = false;
279
+ keys.forEach((key) => {
280
+ let ele = record[key];
281
+ if (ele && typeof ele === 'string' && ele.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
282
+ flag = true;
283
+ }
284
+ });
285
+ return flag;
286
+ });
287
+ }, [patients, query]);
288
+
289
+ useEffect(() => {
290
+ if (filtered) {
291
+ // Export uses the visible display column structure and appends the same
292
+ // summary row that appears in the table when summary columns are enabled.
293
+ // Uses `filtered` (not the raw `patients`) so the exported file matches
294
+ // whatever the search box currently shows on screen.
295
+ const exportCols = cols.map((col) => {
296
+ if (col.title && typeof col.title === 'object' && col.title.props) {
297
+ return { ...col, title: col.title.props.title };
298
+ }
299
+ return col;
300
+ });
301
+ const summaryCols = columns.filter((col) => col.enable_summary);
302
+ let dataToExport = [...filtered];
303
+
304
+ if (summaryCols.length > 0) {
305
+ const summaryValues = calculateSummaryValues(summaryCols, filtered);
306
+ const summaryRow = { isSummaryRow: true };
307
+
308
+ cols.forEach((col) => {
309
+ const colKey = col.field || col.key || col.dataIndex;
310
+ if (colKey && !summaryRow[colKey]) {
311
+ summaryRow[colKey] = '';
312
+ }
313
+
314
+ if (summaryValues[col.field] !== undefined) {
315
+ summaryRow[col.field] = summaryValues[col.field];
316
+ } else {
317
+ const captionConfig = columns.find((c) => col.field && c.caption_field === col.field);
318
+ if (captionConfig) {
319
+ summaryRow[col.field] = captionConfig.summary_caption || '';
320
+ }
321
+ }
322
+ });
323
+ dataToExport.push(summaryRow);
324
+ }
325
+ let exportDatas = getExportData(dataToExport, exportCols);
326
+
327
+ if (exportDatas.exportDataColumns.length && exportDatas.exportDataHeaders.length) {
328
+ setExportData({ exportDatas });
329
+ }
330
+ }
331
+ }, [filtered, columns]);
332
+
333
+ /**
334
+ * Handles successful QR scans by looking up a matching record and navigating
335
+ * through the first configured action column.
336
+ *
337
+ * @param {string} code
338
+ */
339
+ const handleScanSuccess = (code) => {
340
+ const matched = filtered.filter((patient) => patient[barcodeFilterKey] === code);
341
+ if (matched.length) {
342
+ const patient = matched[0];
343
+ message.success(`Match found for ${code}, redirecting...`);
344
+ const actionColumn = columns.find((col) => col.field === 'action') || columns.find((col) => col.type === 'action');
345
+ if (actionColumn) {
346
+ const redirectLink = getRedirectLink(actionColumn, patient,CustomComponents);
347
+ window.location.href = redirectLink;
348
+ }
349
+ } else {
350
+ Modal.warning({
351
+ title: 'No matching records.',
352
+ content: `No match for scanned code: ${code}`,
353
+ });
354
+ }
355
+ };
356
+
357
+ /**
358
+ * Opens a reporting dashboard action component declared in `buttonAttributes`.
359
+ *
360
+ * @param {Object} button
361
+ */
362
+ const handleOpenEdit = (button) => {
363
+ const componentName = button.component;
364
+ const ComponentToRender = ReportingDashboardComp[componentName];
365
+ if (!ComponentToRender) {
366
+ console.error(`Component ${componentName} not found!`);
367
+ return;
368
+ }
369
+ setSingle({});
370
+ setActiveComponent(() => ComponentToRender);
371
+ setVisible(true);
372
+ };
373
+
374
+ /**
375
+ * Computes summary values for the current page based on per-column summary
376
+ * configuration.
377
+ *
378
+ * Supported functions:
379
+ * - `sum`
380
+ * - `count`
381
+ * - `avg`
382
+ * - `min`
383
+ * - `max`
384
+ *
385
+ * @param {Array<Object>} summaryCols
386
+ * @param {Array<Object>} pageData
387
+ * @returns {Object.<string, number>}
388
+ */
389
+ function calculateSummaryValues(summaryCols, pageData) {
390
+ const summaryValues = {};
391
+ summaryCols.forEach((col) => {
392
+ const field = col.field;
393
+ if (col.function === 'sum') {
394
+ summaryValues[field] = pageData.reduce((total, row) => total + Number(row[field] || 0), 0);
395
+ }
396
+ if (col.function === 'count') {
397
+ summaryValues[field] = pageData.length;
398
+ }
399
+ if (col.function === 'avg') {
400
+ const total = pageData.reduce((sum, row) => sum + Number(row[field] || 0), 0);
401
+ summaryValues[field] = pageData.length ? total / pageData.length : 0;
402
+ }
403
+ if (col.function === 'min') {
404
+ const values = pageData.map((row) => Number(row[field] || 0));
405
+ summaryValues[field] = values.length ? Math.min(...values) : 0;
406
+ }
407
+ if (col.function === 'max') {
408
+ const values = pageData.map((row) => Number(row[field] || 0));
409
+ summaryValues[field] = values.length ? Math.max(...values) : 0;
410
+ }
411
+ });
412
+ return summaryValues;
413
+ }
414
+
415
+ return (
416
+ <>
417
+ <div
418
+ className="table-header"
419
+ style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, paddingTop: 10, paddingBottom: 10 }}
420
+ >
421
+ <div className="table-right" style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, flexWrap: 'wrap' }}>
422
+ <div className="table-search" style={{ minWidth: 240, width: 280, maxWidth: '100%', flex: '0 0 auto' }}>
423
+ <Search placeholder="Enter Search Value" allowClear onChange={onSearch} />
424
+ </div>
425
+ {showScanner && (
426
+ <Button size="small" type="primary" icon={<QrcodeOutlined />} onClick={() => setScannerVisible(true)}>
427
+ Scan QR
428
+ </Button>
429
+ )}
430
+ {Array.isArray(buttonAttributes) &&
431
+ buttonAttributes.map((btn, index) => (
432
+ <Button key={index} size="small" type="primary" style={{ marginLeft: 8 }} onClick={() => handleOpenEdit(btn)}>
433
+ {btn.title}
434
+ </Button>
435
+ ))}
436
+ <Modal open={visible} onCancel={() => setVisible(false)} footer={null} destroyOnClose width={950} style={{ top: 10 }}>
437
+ {ActiveComponent && (
438
+ <ActiveComponent
439
+ formContent={single}
440
+ callback={() => {
441
+ setVisible(false);
442
+ refresh && refresh();
443
+ shouldFetchData ? loadIndependentData() : fetchReportData && fetchReportData();
444
+ }}
445
+ />
446
+ )}
447
+ </Modal>
448
+ <Modal open={isScannerVisible} title="Scan QR Code" footer={null} onCancel={() => setScannerVisible(false)} destroyOnClose>
449
+ <QrScanner onScanSuccess={handleScanSuccess} onClose={() => setScannerVisible(false)} />
450
+ </Modal>
451
+ <div>
452
+ {exportData.exportDatas && !isNuradesk && (
453
+ <ExportReactCSV
454
+ title={config.caption}
455
+ headers={exportData.exportDatas.exportDataHeaders}
456
+ csvData={exportData.exportDatas.exportDataColumns}
457
+ fileName={`${config.caption || 'Report'}.xlsx`}
458
+ pdfFileName={`${config.caption || 'Report'}.pdf`}
459
+ dropdown
460
+ />
461
+ )}
462
+ </div>
463
+ </div>
464
+ </div>
465
+ <div>
466
+ <Card>
467
+ {loading ? (
468
+ <Skeleton active paragraph={{ rows: 6 }} />
469
+ ) : (
470
+ <TableComponent
471
+ size="small"
472
+ scroll={{ x: 'max-content', y: '60vh' }}
473
+ rowKey={(record) => record.OpNo}
474
+ dataSource={filtered}
475
+ columns={cols}
476
+ sticky
477
+ pagination={false}
478
+ summary={(pageData) => {
479
+ const summaryCols = columns.filter((col) => col.enable_summary);
480
+ if (!summaryCols.length) return null;
481
+ const summaryValues = calculateSummaryValues(summaryCols, pageData);
482
+ return (
483
+ <Table.Summary.Row className="report-summary-row">
484
+ {cols.map((col, index) => {
485
+ if (summaryValues[col.field] !== undefined) {
486
+ return (
487
+ <Table.Summary.Cell key={index}>
488
+ <strong style={{ fontWeight: 900 }}>{summaryValues[col.field]}</strong>
489
+ </Table.Summary.Cell>
490
+ );
491
+ }
492
+ const captionConfig = columns.find((c) => col.field && c.caption_field === col.field);
493
+ if (captionConfig) {
494
+ return (
495
+ <Table.Summary.Cell key={index}>
496
+ <strong style={{ fontWeight: 900 }}>{captionConfig.summary_caption || ''}</strong>
497
+ </Table.Summary.Cell>
498
+ );
499
+ }
500
+ return <Table.Summary.Cell key={index} />;
501
+ })}
502
+ </Table.Summary.Row>
503
+ );
504
+ }}
505
+ />
506
+ )}
507
+ <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 8 }}>
508
+ <Pagination
509
+ showSizeChanger
510
+ current={pagination?.current}
511
+ pageSize={pagination?.pageSize}
512
+ total={pagination?.total}
513
+ pageSizeOptions={[20, 30, 50, 100]}
514
+ onChange={(page, pageSize) => handlePaginationInternal({ current: page, pageSize })}
515
+ />
516
+ </div>
517
+ <p className="size-hint">{patients ? patients.length : 0} records.</p>
518
+ </Card>
519
+ </div>
520
+ </>
521
+ );
522
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-soxo-bootstrap-core",
3
- "version": "2.6.46-dev.5",
3
+ "version": "2.6.46-dev.7",
4
4
  "description": "All the Core Components for you to start",
5
5
  "keywords": [
6
6
  "all in one"