ui-soxo-bootstrap-core 2.6.30 → 2.6.32-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,519 @@
1
+ import React, { useState, useEffect } 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
+ useEffect(() => {
274
+ if (patients) {
275
+ // Export uses the visible display column structure and appends the same
276
+ // summary row that appears in the table when summary columns are enabled.
277
+ const exportCols = cols.map((col) => {
278
+ if (col.title && typeof col.title === 'object' && col.title.props) {
279
+ return { ...col, title: col.title.props.title };
280
+ }
281
+ return col;
282
+ });
283
+ const summaryCols = columns.filter((col) => col.enable_summary);
284
+ let dataToExport = [...patients];
285
+
286
+ if (summaryCols.length > 0) {
287
+ const summaryValues = calculateSummaryValues(summaryCols, patients);
288
+ const summaryRow = { isSummaryRow: true };
289
+
290
+ cols.forEach((col) => {
291
+ const colKey = col.field || col.key || col.dataIndex;
292
+ if (colKey && !summaryRow[colKey]) {
293
+ summaryRow[colKey] = '';
294
+ }
295
+
296
+ if (summaryValues[col.field] !== undefined) {
297
+ summaryRow[col.field] = summaryValues[col.field];
298
+ } else {
299
+ const captionConfig = columns.find((c) => col.field && c.caption_field === col.field);
300
+ if (captionConfig) {
301
+ summaryRow[col.field] = captionConfig.summary_caption || '';
302
+ }
303
+ }
304
+ });
305
+ dataToExport.push(summaryRow);
306
+ }
307
+ let exportDatas = getExportData(dataToExport, exportCols);
308
+
309
+ if (exportDatas.exportDataColumns.length && exportDatas.exportDataHeaders.length) {
310
+ setExportData({ exportDatas });
311
+ }
312
+ }
313
+ }, [patients, columns]);
314
+
315
+ let filtered = patients;
316
+ if (patients && query) {
317
+ filtered = patients.filter((record) => {
318
+ let keys = Object.keys(record);
319
+ let flag = false;
320
+ keys.forEach((key) => {
321
+ let ele = record[key];
322
+ if (ele && typeof ele === 'string' && ele.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
323
+ flag = true;
324
+ }
325
+ });
326
+ return flag;
327
+ });
328
+ }
329
+
330
+ /**
331
+ * Handles successful QR scans by looking up a matching record and navigating
332
+ * through the first configured action column.
333
+ *
334
+ * @param {string} code
335
+ */
336
+ const handleScanSuccess = (code) => {
337
+ const matched = filtered.filter((patient) => patient[barcodeFilterKey] === code);
338
+ if (matched.length) {
339
+ const patient = matched[0];
340
+ message.success(`Match found for ${code}, redirecting...`);
341
+ const actionColumn = columns.find((col) => col.field === 'action') || columns.find((col) => col.type === 'action');
342
+ if (actionColumn) {
343
+ const redirectLink = getRedirectLink(actionColumn, patient,CustomComponents);
344
+ window.location.href = redirectLink;
345
+ }
346
+ } else {
347
+ Modal.warning({
348
+ title: 'No matching records.',
349
+ content: `No match for scanned code: ${code}`,
350
+ });
351
+ }
352
+ };
353
+
354
+ /**
355
+ * Opens a reporting dashboard action component declared in `buttonAttributes`.
356
+ *
357
+ * @param {Object} button
358
+ */
359
+ const handleOpenEdit = (button) => {
360
+ const componentName = button.component;
361
+ const ComponentToRender = ReportingDashboardComp[componentName];
362
+ if (!ComponentToRender) {
363
+ console.error(`Component ${componentName} not found!`);
364
+ return;
365
+ }
366
+ setSingle({});
367
+ setActiveComponent(() => ComponentToRender);
368
+ setVisible(true);
369
+ };
370
+
371
+ /**
372
+ * Computes summary values for the current page based on per-column summary
373
+ * configuration.
374
+ *
375
+ * Supported functions:
376
+ * - `sum`
377
+ * - `count`
378
+ * - `avg`
379
+ * - `min`
380
+ * - `max`
381
+ *
382
+ * @param {Array<Object>} summaryCols
383
+ * @param {Array<Object>} pageData
384
+ * @returns {Object.<string, number>}
385
+ */
386
+ function calculateSummaryValues(summaryCols, pageData) {
387
+ const summaryValues = {};
388
+ summaryCols.forEach((col) => {
389
+ const field = col.field;
390
+ if (col.function === 'sum') {
391
+ summaryValues[field] = pageData.reduce((total, row) => total + Number(row[field] || 0), 0);
392
+ }
393
+ if (col.function === 'count') {
394
+ summaryValues[field] = pageData.length;
395
+ }
396
+ if (col.function === 'avg') {
397
+ const total = pageData.reduce((sum, row) => sum + Number(row[field] || 0), 0);
398
+ summaryValues[field] = pageData.length ? total / pageData.length : 0;
399
+ }
400
+ if (col.function === 'min') {
401
+ const values = pageData.map((row) => Number(row[field] || 0));
402
+ summaryValues[field] = values.length ? Math.min(...values) : 0;
403
+ }
404
+ if (col.function === 'max') {
405
+ const values = pageData.map((row) => Number(row[field] || 0));
406
+ summaryValues[field] = values.length ? Math.max(...values) : 0;
407
+ }
408
+ });
409
+ return summaryValues;
410
+ }
411
+
412
+ return (
413
+ <>
414
+ <div
415
+ className="table-header"
416
+ style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, paddingTop: 10, paddingBottom: 10 }}
417
+ >
418
+ <div className="table-right" style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, flexWrap: 'wrap' }}>
419
+ <div className="table-search" style={{ minWidth: 240, width: 280, maxWidth: '100%', flex: '0 0 auto' }}>
420
+ <Search placeholder="Enter Search Value" allowClear onChange={onSearch} />
421
+ </div>
422
+ {showScanner && (
423
+ <Button size="small" type="primary" icon={<QrcodeOutlined />} onClick={() => setScannerVisible(true)}>
424
+ Scan QR
425
+ </Button>
426
+ )}
427
+ {Array.isArray(buttonAttributes) &&
428
+ buttonAttributes.map((btn, index) => (
429
+ <Button key={index} size="small" type="primary" style={{ marginLeft: 8 }} onClick={() => handleOpenEdit(btn)}>
430
+ {btn.title}
431
+ </Button>
432
+ ))}
433
+ <Modal open={visible} onCancel={() => setVisible(false)} footer={null} destroyOnClose width={950} style={{ top: 10 }}>
434
+ {ActiveComponent && (
435
+ <ActiveComponent
436
+ formContent={single}
437
+ callback={() => {
438
+ setVisible(false);
439
+ refresh && refresh();
440
+ shouldFetchData ? loadIndependentData() : fetchReportData && fetchReportData();
441
+ }}
442
+ />
443
+ )}
444
+ </Modal>
445
+ <Modal open={isScannerVisible} title="Scan QR Code" footer={null} onCancel={() => setScannerVisible(false)} destroyOnClose>
446
+ <QrScanner onScanSuccess={handleScanSuccess} onClose={() => setScannerVisible(false)} />
447
+ </Modal>
448
+ <div>
449
+ {exportData.exportDatas && !isNuradesk && (
450
+ <ExportReactCSV
451
+ title={config.caption}
452
+ fileName={`${(config.caption || 'Report').trim().replace(/\s+/g, '_')}_${moment().format('YYYY-MM-DD-HH-mm-ss-SSS')}.xlsx`}
453
+ headers={exportData.exportDatas.exportDataHeaders}
454
+ csvData={exportData.exportDatas.exportDataColumns}
455
+ />
456
+ )}
457
+ </div>
458
+ </div>
459
+ </div>
460
+ <div>
461
+ <Card>
462
+ {loading ? (
463
+ <Skeleton active paragraph={{ rows: 6 }} />
464
+ ) : (
465
+ <TableComponent
466
+ size="small"
467
+ // Change x to '100%' or true to make it responsive to the container width
468
+ // Your vertical logic (adjust '10' based on how many rows fit on your screen)
469
+ scroll={{ x: 'max-content', y: (filtered?.length || 0) > 11 ? 400 : undefined }}
470
+ rowKey={(record) => record.OpNo}
471
+ dataSource={filtered}
472
+ columns={cols}
473
+ sticky
474
+ pagination={false}
475
+ summary={(pageData) => {
476
+ const summaryCols = columns.filter((col) => col.enable_summary);
477
+ if (!summaryCols.length) return null;
478
+ const summaryValues = calculateSummaryValues(summaryCols, pageData);
479
+ return (
480
+ <Table.Summary.Row className="report-summary-row">
481
+ {cols.map((col, index) => {
482
+ if (summaryValues[col.field] !== undefined) {
483
+ return (
484
+ <Table.Summary.Cell key={index}>
485
+ <strong style={{ fontWeight: 900 }}>{summaryValues[col.field]}</strong>
486
+ </Table.Summary.Cell>
487
+ );
488
+ }
489
+ const captionConfig = columns.find((c) => col.field && c.caption_field === col.field);
490
+ if (captionConfig) {
491
+ return (
492
+ <Table.Summary.Cell key={index}>
493
+ <strong style={{ fontWeight: 900 }}>{captionConfig.summary_caption || ''}</strong>
494
+ </Table.Summary.Cell>
495
+ );
496
+ }
497
+ return <Table.Summary.Cell key={index} />;
498
+ })}
499
+ </Table.Summary.Row>
500
+ );
501
+ }}
502
+ />
503
+ )}
504
+ <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 8 }}>
505
+ <Pagination
506
+ showSizeChanger
507
+ current={pagination?.current}
508
+ pageSize={pagination?.pageSize}
509
+ total={pagination?.total}
510
+ pageSizeOptions={[20, 30, 50, 100]}
511
+ onChange={(page, pageSize) => handlePaginationInternal({ current: page, pageSize })}
512
+ />
513
+ </div>
514
+ <p className="size-hint">{patients ? patients.length : 0} records.</p>
515
+ </Card>
516
+ </div>
517
+ </>
518
+ );
519
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-soxo-bootstrap-core",
3
- "version": "2.6.30",
3
+ "version": "2.6.32-dev.0",
4
4
  "description": "All the Core Components for you to start",
5
5
  "keywords": [
6
6
  "all in one"