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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,31 +2,18 @@ import React, { useState, useEffect, useContext, useRef } from 'react';
2
2
 
3
3
  import { Table, Skeleton, Input, Modal, message, Tag } from 'antd';
4
4
 
5
- import { QrcodeOutlined } from '@ant-design/icons';
6
-
7
- import { Location, FormCreator, GlobalContext, ExportReactCSV, getExportData, Card, TableComponent, QrScanner } from './../../../../lib/';
5
+ import { Location, FormCreator, GlobalContext, Card } from './../../../../lib/';
8
6
 
9
7
  import { CoreScripts } from './../../../../models/';
10
8
 
11
9
  import moment from 'moment-timezone';
12
10
 
13
- import Button from '../../../../lib/elements/basic/button/button';
14
-
15
11
  import './reporting-dashboard.scss';
16
12
 
17
13
  // import MenuDashBoard from '../../../../pages/homepage-api/menu-dashboard';
18
14
  import MenuDashBoardComponent from '../../../../lib/elements/basic/menu-dashboard/menu-dashboard';
19
- import { useHistory } from 'react-router-dom';
20
- import * as ReportingDashboardComp from '../index';
21
- import buildDisplayColumns from './display-columns/build-display-columns';
22
- import { getRedirectLink } from './display-columns/display-cell-renderer';
23
- import AdvancedSearchSelect from './adavance-search/advance-search';
24
-
25
- // import { isPdfFile } from 'pdfjs-dist';
26
15
 
27
- var genericComponents = require('./../../../../lib');
28
-
29
- const { Search } = Input;
16
+ import ReportingTable from './reporting-table';
30
17
 
31
18
  /**
32
19
  * ReportingDashboard component renders the dashboard and handles patient details,
@@ -56,7 +43,7 @@ export default function ReportingDashboard({
56
43
  const [config, setConfig] = useState({});
57
44
 
58
45
  // State to manage the layout of the form
59
- const [formLayout, setFormLayout] = useState('vertical');
46
+ const [formLayout] = useState('inline');
60
47
 
61
48
  const [loading, setLoading] = useState(true);
62
49
 
@@ -79,7 +66,7 @@ export default function ReportingDashboard({
79
66
 
80
67
  //In case of reports from core_script , there will be id from params
81
68
  // In case of normal menu we need to take id from props
82
- let id = reportId ? reportId : match.params.id;
69
+ let id = reportId ? reportId : match?.params?.id;
83
70
 
84
71
  const { CustomModels = {} } = useContext(GlobalContext);
85
72
 
@@ -87,9 +74,9 @@ export default function ReportingDashboard({
87
74
 
88
75
  const [columns, setColumns] = useState([]); // To set columns
89
76
 
90
- const [summaryColumns, setSummaryColumns] = useState([]);
77
+ const [, setSummaryColumns] = useState([]);
91
78
 
92
- const [patients, setPatients] = useState([]); //Patients list array
79
+ const [reportRequestPayload, setReportRequestPayload] = useState(null);
93
80
 
94
81
  const urlParams = Location.search();
95
82
 
@@ -110,7 +97,6 @@ export default function ReportingDashboard({
110
97
  * @returns {Promise<void>} A promise that resolves when the patient details have been fetched and the state has been updated.
111
98
  */
112
99
  async function getPatientDetails(idOverride) {
113
- setPatients([]);
114
100
  const fetchId = idOverride || id;
115
101
  await CoreScripts.getRecord({ id: fetchId, dbPtr }).then(async ({ result }) => {
116
102
  // Check if display columns are provided from backend
@@ -270,87 +256,38 @@ export default function ReportingDashboard({
270
256
  // Refresh patient details.
271
257
 
272
258
  function refresh() {
273
- getPatientDetails();
259
+ getPatientDetails(scriptId.current || id);
274
260
  }
275
261
 
276
- const fetchReportData = async (id, values, dbPtr, pagination, parsedColumns) => {
277
- const { current, pageSize } = pagination || {};
278
- // If card script id is exist load that id otherwise load id
279
- const coreScriptId = scriptId.current ? scriptId.current : id;
280
- const normalizedColumns = Array.isArray(parsedColumns) ? parsedColumns : Array.isArray(columns) ? columns : [];
262
+ const buildReportRequestPayload = (values = {}, paginationOverride) => {
263
+ const pager = paginationOverride || pagination;
264
+ const formattedValues = {};
281
265
 
282
- setLoading(true);
283
- try {
284
- // Prepare payload for backend: format only here
285
- const formattedValues = {};
286
- Object.keys(values).forEach((key) => {
287
- const val = values[key];
288
- formattedValues[key] = moment.isMoment(val) ? val.format('YYYY-MM-DD') : val;
289
- });
290
- // Pagination Data
291
- const paginationData = {
292
- page: pagination.current || 1,
293
- limit: pagination.pageSize || 10,
294
- };
295
- // Combine form data + pagination
296
- let formBody = {
266
+ Object.keys(values || {}).forEach((key) => {
267
+ const val = values[key];
268
+ formattedValues[key] = moment.isMoment(val) ? val.format('YYYY-MM-DD') : val;
269
+ });
270
+
271
+ const paginationData = {
272
+ page: pager.current || 1,
273
+ limit: pager.pageSize || 10,
274
+ };
275
+
276
+ if (scope) {
277
+ return {
297
278
  body: {
298
- ...formattedValues,
279
+ ...scope,
299
280
  ...paginationData,
300
281
  },
301
282
  };
302
- // Optional override if `scope` exists
303
- if (scope) {
304
- formBody = { body: { ...scope, ...paginationData } };
305
- }
306
-
307
- setReportMailRequest({
308
- scriptId: coreScriptId,
309
- inputParameters: formBody,
310
- });
311
-
312
- // Fetch result
313
- const result = await CoreScripts.getReportingLisitng(coreScriptId, formBody, dbPtr);
314
-
315
- const apiData = Array.isArray(result) ? result : Array.isArray(result?.result) ? result.result : [];
316
-
317
- // Handle both result formats
318
- let resultDetails = apiData[0] || [];
319
- if (result?.result && result?.result[0]) {
320
- resultDetails = result.result[0];
321
- }
322
- // Update patients
323
- setPatients(resultDetails || []);
324
-
325
- // When display_columns is missing, build columns from the response keys.
326
- if (normalizedColumns.length === 0 && resultDetails.length > 0) {
327
- // Create columns dynamically from resultDetails keys
328
- setColumns((prev) => {
329
- if (prev.length > 0) return prev;
330
- return Object.keys(resultDetails[0]).map((key) => ({
331
- title: key,
332
- field: key,
333
- }));
334
- });
335
- }
336
-
337
- if (result.length) {
338
- // Set Pgination data into URL
339
- Location.search({ ...Location.search(), current, pageSize });
340
-
341
- setPagination((prev) => ({
342
- ...prev,
343
- current: pagination.current,
344
- pageSize: pagination.pageSize,
345
- total: resultDetails?.[0]?.TotalCount ?? pagination?.total,
346
- }));
347
- }
348
- } catch (error) {
349
- console.error('Error fetching report data:', error);
350
- } finally {
351
- // Always runs, success or error
352
- setLoading(false);
353
283
  }
284
+
285
+ return {
286
+ body: {
287
+ ...formattedValues,
288
+ ...paginationData,
289
+ },
290
+ };
354
291
  };
355
292
 
356
293
  const handleSubmit = (values) => {
@@ -561,17 +498,12 @@ export default function ReportingDashboard({
561
498
 
562
499
  // Call API
563
500
  try {
564
- await fetchReportData(id, values, dbPtr, paginationData, parsedColumns);
565
- } finally {
566
- setLoading(false);
567
- setCardLoading(false);
568
- }
569
- };
570
-
571
- // Pagination Handler
572
- const handlePagination = async (newPagination) => {
573
- try {
574
- await fetchReportData(id, formContents, dbPtr, newPagination);
501
+ setPagination((prev) => ({
502
+ ...prev,
503
+ current: paginationData.current,
504
+ pageSize: paginationData.pageSize,
505
+ }));
506
+ setReportRequestPayload(buildReportRequestPayload(values, paginationData));
575
507
  } finally {
576
508
  setLoading(false);
577
509
  setCardLoading(false);
@@ -659,454 +591,36 @@ export default function ReportingDashboard({
659
591
  ) : null}
660
592
  </div>
661
593
 
662
- {/** GuestList component start*/}
663
- <GuestList
664
- patients={patients}
594
+ <ReportingTable
665
595
  columns={columns}
666
- summaryColumns={summaryColumns}
667
596
  isFixedIndex={isFixedIndex}
668
597
  showScanner={showScanner}
598
+ reportId={reportId}
599
+ requestId={scriptId.current ? scriptId.current : id}
600
+ requestPayload={reportRequestPayload}
601
+ dbPtr={dbPtr}
669
602
  barcodeFilterKey={barcodeFilterKey}
670
- CustomComponents={{ ...CustomComponents, ...genericComponents, ...ReportingDashboardComp }}
603
+ CustomComponents={CustomComponents}
671
604
  refresh={refresh}
672
605
  config={config}
606
+
673
607
  loading={cardLoading}
674
608
  pagination={pagination}
675
- handlePagination={handlePagination}
609
+ onPaginationChange={(nextPagination) => {
610
+ Location.search({
611
+ ...Location.search(),
612
+ current: nextPagination.current,
613
+ pageSize: nextPagination.pageSize,
614
+ });
615
+ setPagination((prev) => ({
616
+ ...prev,
617
+ ...nextPagination,
618
+ }));
619
+ }}
676
620
  attributes={attributes}
677
- selectedSearchFields={selectedSearchFields}
678
- handleRemoveSearchField={handleRemoveSearchField}
679
- fetchReportData={(paginationUpdate) => fetchReportData(id, formContents, dbPtr, paginationUpdate || pagination)}
680
- reportScriptId={reportMailRequest.scriptId}
681
- reportInputParameters={reportMailRequest.inputParameters}
682
621
  />
683
- {/** GuestList component end*/}
684
622
  </>
685
623
  )}
686
624
  </Card>
687
625
  );
688
626
  }
689
-
690
- /**
691
- * Filters report records using the same search text that drives the table.
692
- *
693
- * @param {Array<Object>} records - Report rows.
694
- * @param {string} searchText - Current table search text.
695
- * @returns {Array<Object>} Rows matching the search text.
696
- */
697
- function filterReportRecords(records, searchText) {
698
- if (!Array.isArray(records)) return [];
699
-
700
- const query = String(searchText || '').trim().toLowerCase();
701
-
702
- if (!query) return records;
703
-
704
- return records.filter((record) =>
705
- Object.values(record).some((value) => {
706
- if (value === undefined || value === null || typeof value === 'object') return false;
707
-
708
- return String(value).toLowerCase().indexOf(query) !== -1;
709
- })
710
- );
711
- }
712
-
713
- /**
714
- *
715
- * @param root0
716
- * @param root0.patients
717
- * @param root0.CustomComponents
718
- * @param root0.summaryColumns
719
- * @param root0.refresh
720
- * @param root0.isFixedIndex
721
- * @returns {*}
722
- */
723
- //Renders a table displaying a list of patients with dynamic columns
724
- function GuestList({
725
- patients,
726
- columns,
727
- loading,
728
- CustomComponents,
729
- refresh,
730
- isFixedIndex,
731
- barcodeFilterKey,
732
- showScanner,
733
- config,
734
- pagination,
735
- handlePagination,
736
- attributes,
737
- selectedSearchFields,
738
- handleRemoveSearchField,
739
- fetchReportData,
740
- reportScriptId,
741
- reportInputParameters,
742
- }) {
743
- /**
744
- * @param {*} propValues
745
- */
746
- const propValues = (attributes && JSON.parse(attributes)) || {};
747
-
748
- const { buttonAttributes = [] } = propValues;
749
-
750
- var [query, setQuery] = useState('');
751
-
752
- const [exportData, setExportData] = useState({});
753
-
754
- // const [data, setData] = useState([]);
755
-
756
- //visibility of the QR scanner modal.
757
- const [isScannerVisible, setScannerVisible] = useState(false);
758
-
759
- // Stores the patients filtered specifically by QR scan match.
760
- const [filteredPatients, setFilteredPatients] = useState([]); // Show all initially
761
-
762
- // patient object to redirect to upon successful QR scan.
763
- const [redirectPatient, setRedirectPatient] = useState(null);
764
-
765
- const [visible, setVisible] = useState(false);
766
-
767
- const [ActiveComponent, setActiveComponent] = useState(null);
768
-
769
- let history = useHistory();
770
-
771
- const { isMobile, dispatch } = useContext(GlobalContext);
772
- const [single, setSingle] = useState({});
773
- const otherDetails = config.other_details1 ? JSON.parse(config.other_details1) : {};
774
-
775
- // const otherDetails = config.other_details1 ? JSON.parse(config.other_details1) : {};
776
- // const [view, setView] = useState(isMobile ? true : false); //Need to check this condition
777
- const cols = buildDisplayColumns({
778
- columns,
779
- patients,
780
- isFixedIndex,
781
- CustomComponents,
782
- refresh,
783
- otherDetails,
784
- });
785
-
786
- /**
787
- *
788
- * @param {*} result
789
- */
790
-
791
- // function changeView(result) {
792
- // setView(result);
793
- // }
794
-
795
- /**
796
- *
797
- * @param {*} event
798
- */
799
-
800
- function onSearch(event) {
801
- setQuery(event.target.value);
802
- }
803
-
804
- /**
805
- *
806
- */
807
-
808
- useEffect(() => {
809
- //Cheaking if there is patient data exists
810
- if (patients) {
811
- // let data = patients?.map((entry) => {
812
- // entry.rowIndex = entry.opb_id;
813
-
814
- // entry.dispatch = dispatch;
815
-
816
- // return entry;
817
- // });
818
-
819
- // setData(data);
820
-
821
- // Define export data
822
- // Sanitize cols for export to ensure titles are strings
823
- const exportCols = cols.map((col) => {
824
- if (col.title && typeof col.title === 'object' && col.title.props) {
825
- return { ...col, title: col.title.props.title };
826
- }
827
- return col;
828
- });
829
- const summaryCols = columns.filter((col) => col.enable_summary);
830
- let dataToExport = [...filterReportRecords(patients, query)];
831
-
832
- if (summaryCols.length > 0) {
833
- // Build one synthetic row for CSV export that mirrors the table layout:
834
- // numeric summary cells are populated from `calculateSummaryValues`, while
835
- // non-summary columns stay blank unless a configured caption should be shown.
836
- const summaryValues = calculateSummaryValues(summaryCols, dataToExport);
837
- const summaryRow = { isSummaryRow: true };
838
-
839
- cols.forEach((col, index) => {
840
- // Start each export column empty so the appended row keeps the same shape
841
- // as the data rows and does not leak index/helper values into the export.
842
- const colKey = col.field || col.key || col.dataIndex;
843
- if (colKey && !summaryRow[colKey]) {
844
- summaryRow[colKey] = '';
845
- }
846
-
847
- if (summaryValues[col.field] !== undefined) {
848
- // Fill columns that have an aggregate configured (sum, count, avg, etc.).
849
- summaryRow[col.field] = summaryValues[col.field];
850
- } else {
851
- // If this column is marked as the caption target for a summary column,
852
- // place the configured label (for example "Total") into that cell.
853
- const captionConfig = columns.find((c) => col.field && c.caption_field === col.field);
854
- if (captionConfig) {
855
- summaryRow[col.field] = captionConfig.summary_caption || '';
856
- }
857
- }
858
- });
859
- dataToExport.push(summaryRow);
860
- }
861
- let exportDatas = getExportData(dataToExport, exportCols);
862
-
863
- if (exportDatas.exportDataHeaders.length) {
864
- setExportData({ exportDatas });
865
- } else {
866
- setExportData({});
867
- }
868
- }
869
- }, [patients, columns, query]);
870
-
871
- let filtered = filterReportRecords(patients, query);
872
-
873
- /**
874
- * Checks for a match in the filtered patient list based on a scanned code,
875
- * updates the relevant state if a match is found, and redirects to the
876
- * patient's detail page. Displays a warning if no match is found.
877
- *
878
- * @param {string} code - The scanned code to match against a specific field of each patient.
879
- */
880
-
881
- const handleScanSuccess = (code) => {
882
- // Filters patients based on the scanned code and the selected barcode key(using attributes 'barcodeFilterKey')
883
- const matched = filtered.filter((patient) => patient[barcodeFilterKey] === code);
884
-
885
- if (matched.length) {
886
- const patient = matched[0];
887
- setFilteredPatients(matched);
888
- setRedirectPatient(matched);
889
- message.success(`Match found for ${code}, redirecting...`);
890
-
891
- const actionColumn = columns.find((col) => col.field === 'action') || columns.find((col) => col.type === 'action');
892
- if (actionColumn) {
893
- const redirectLink = getRedirectLink(actionColumn, patient);
894
- // history.push(redirectLink);
895
- window.location.href = redirectLink;
896
- }
897
- } else {
898
- Modal.warning({
899
- title: 'No matching records.',
900
- content: `No match for scanned code: ${code}`,
901
- });
902
- }
903
- };
904
-
905
- //open the edit modal
906
- const handleOpenEdit = (button) => {
907
- const componentName = button.component;
908
- const ComponentToRender = ReportingDashboardComp[componentName];
909
-
910
- if (!ComponentToRender) {
911
- console.error(`Component ${componentName} not found!`);
912
- return;
913
- }
914
-
915
- setSingle({});
916
- setActiveComponent(() => ComponentToRender);
917
- setVisible(true);
918
- };
919
-
920
- // close the edit modal
921
- const handleCloseEdit = () => {
922
- setShowEdit(false);
923
- };
924
- /**
925
- * Calculates aggregate values for the configured summary columns.
926
- *
927
- * Each summary definition contributes one value keyed by its `field`. Missing
928
- * row values are treated as `0` for numeric operations so the table summary and
929
- * export summary row can be built from the same result object.
930
- *
931
- * Supported functions:
932
- * `sum` - totals all numeric values in the field.
933
- * `count` - returns the number of rows in the current dataset.
934
- * `avg` - returns the arithmetic mean of the field values.
935
- * `min` - returns the smallest numeric value in the field.
936
- * `max` - returns the largest numeric value in the field.
937
- *
938
- * @param {Array<Object>} summaryCols - Column configs with `field` and `function`.
939
- * @param {Array<Object>} pageData - Rows currently being summarized.
940
- * @returns {Object} Aggregate values keyed by field name.
941
- */
942
- function calculateSummaryValues(summaryCols, pageData) {
943
- const summaryValues = {};
944
-
945
- summaryCols.forEach((col) => {
946
- const field = col.field;
947
-
948
- if (col.function === 'sum') {
949
- summaryValues[field] = pageData.reduce((total, row) => total + Number(row[field] || 0), 0);
950
- }
951
-
952
- if (col.function === 'count') {
953
- summaryValues[field] = pageData.length;
954
- }
955
-
956
- if (col.function === 'avg') {
957
- const total = pageData.reduce((sum, row) => sum + Number(row[field] || 0), 0);
958
- summaryValues[field] = pageData.length ? total / pageData.length : 0;
959
- }
960
- if (col.function === 'min') {
961
- const values = pageData.map((row) => Number(row[field] || 0));
962
- summaryValues[field] = values.length ? Math.min(...values) : 0;
963
- }
964
-
965
- if (col.function === 'max') {
966
- const values = pageData.map((row) => Number(row[field] || 0));
967
- summaryValues[field] = values.length ? Math.max(...values) : 0;
968
- }
969
- });
970
-
971
- return summaryValues;
972
- }
973
- return (
974
- <>
975
- <div className="table-header">
976
- <div className="table-left">
977
- {/* {selectedSearchFields?.length > 0 ? (
978
- <div className="search-tags-container">
979
- {selectedSearchFields.map((field) => (
980
- <Tag key={field.field} closable color="blue" onClose={() => handleRemoveSearchField(field.field)}>
981
- {field.caption}
982
- </Tag>
983
- ))}
984
- </div>
985
- ) : null} */}
986
- </div>
987
-
988
- <div className="table-right">
989
- {/* shwoing caption is not correct so this commented */}
990
- {/* <span className="menu-caption">{config.caption}</span> */}
991
- <Search className="table-search-input" placeholder="Enter Search Value" allowClear onChange={onSearch} />
992
- <div className="table-export-button">
993
- {exportData.exportDatas && (
994
- <>
995
- <ExportReactCSV
996
- title={config.caption}
997
- headers={exportData.exportDatas.exportDataHeaders}
998
- csvData={exportData.exportDatas.exportDataColumns}
999
- fileName={`${config.caption || 'Report'}.xlsx`}
1000
- pdfFileName={`${config.caption || 'Report'}.pdf`}
1001
- scriptId={reportScriptId}
1002
- inputParameters={reportInputParameters}
1003
- dropdown
1004
- />
1005
- </>
1006
- )}
1007
- </div>
1008
-
1009
- {/* QR Scan start */}
1010
- {showScanner ? (
1011
- <Button size="small" type="primary" icon={<QrcodeOutlined />} onClick={() => setScannerVisible(true)}>
1012
- Scan QR
1013
- </Button>
1014
- ) : null}
1015
- {/** Add User button */}
1016
- {Array.isArray(buttonAttributes) &&
1017
- buttonAttributes.map((btn, index) => (
1018
- <Button key={index} size="small" type="primary" style={{ marginLeft: 8 }} onClick={() => handleOpenEdit(btn)}>
1019
- {btn.title}
1020
- </Button>
1021
- ))}
1022
-
1023
- <Modal open={visible} onCancel={() => setVisible(false)} footer={null} destroyOnClose width={950} style={{ top: 10 }}>
1024
- {ActiveComponent && (
1025
- <ActiveComponent
1026
- formContent={single}
1027
- callback={() => {
1028
- setVisible(false);
1029
- refresh();
1030
- setVisible(false);
1031
- fetchReportData();
1032
- }}
1033
- // {...dynamicProps}
1034
- />
1035
- )}
1036
- </Modal>
1037
-
1038
- <Modal open={isScannerVisible} title="Scan QR Code" footer={null} onCancel={() => setScannerVisible(false)} destroyOnClose>
1039
- <QrScanner onScanSuccess={handleScanSuccess} onClose={() => setScannerVisible(false)} />
1040
- </Modal>
1041
- {/* QR Scan End */}
1042
- </div>
1043
- </div>
1044
-
1045
- <div>
1046
- <Card>
1047
- {loading ? (
1048
- <>
1049
- <Skeleton active paragraph={{ rows: 6 }} />
1050
- </>
1051
- ) : (
1052
- <TableComponent
1053
- size="small"
1054
- // scroll={{ x: 'max-content' }}
1055
- scroll={{ x: 'max-content', y: '60vh' }}
1056
- rowKey={(record) => record.OpNo}
1057
- dataSource={filtered ? filtered : patients} // In case if there is no filtered values we can use patient data
1058
- columns={cols}
1059
- sticky
1060
- pagination={{
1061
- current: pagination.current,
1062
- pageSize: pagination.pageSize,
1063
- total: pagination.total,
1064
- showSizeChanger: true,
1065
- pageSizeOptions: [20, 30, 50, 100],
1066
- onChange: (page, pageSize) => handlePagination({ current: page, pageSize }),
1067
- }}
1068
- summary={(pageData) => {
1069
- const summaryCols = columns.filter((col) => col.enable_summary);
1070
- if (!summaryCols.length) return null;
1071
- /** calculate summary*/
1072
-
1073
- const summaryValues = calculateSummaryValues(summaryCols, pageData);
1074
-
1075
-
1076
- return (
1077
- <Table.Summary.Row className="report-summary-row">
1078
- {cols.map((col, index) => {
1079
- if (summaryValues[col.field] !== undefined) {
1080
- return (
1081
- <Table.Summary.Cell key={index}>
1082
- <strong style={{ fontWeight: 900 }}>{summaryValues[col.field]}</strong>
1083
- </Table.Summary.Cell>
1084
- );
1085
- }
1086
-
1087
- const captionConfig = columns.find((c) => col.field && c.caption_field === col.field);
1088
- if (captionConfig) {
1089
- return (
1090
- <Table.Summary.Cell key={index}>
1091
- <strong style={{ fontWeight: 900 }}>{captionConfig.summary_caption || ''}</strong>
1092
- </Table.Summary.Cell>
1093
- );
1094
- }
1095
-
1096
- return <Table.Summary.Cell key={index} />;
1097
- })}
1098
- </Table.Summary.Row>
1099
- );
1100
- }}
1101
- />
1102
- )}
1103
-
1104
- {/*If patient data exists show the number else to 0 */}
1105
- <p className="size-hint">{patients ? patients.length : 0} records. </p>
1106
- </Card>
1107
- {/* </> */}
1108
- {/* )} */}
1109
- </div>
1110
- </>
1111
- );
1112
- }