/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
/* eslint-disable import/no-webpack-loader-syntax */
/* eslint-disable import/no-unresolved */
import classnames from 'classnames';
import dayjs from 'dayjs';
import _ from 'lodash';
import React, {
startTransition,
useEffect,
useRef,
useState,
} from 'react';
import {
useHistory,
useParams,
} from 'react-router-dom';
import {
TabContent,
TabPane,
UncontrolledTooltip,
} from 'reactstrap';
import {
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from 'recoil';
import { t } from 'ttag';
import {
clearInterval,
setInterval,
} from 'worker-timers';
import {
Thread,
Worker,
spawn,
} from 'threads';
import beatWorker from 'threads-plugin/dist/loader?name=beats!../../Worker/beats.worker';
import fetchHolterBeatEventsCount from '../../Apollo/Functions/fetchHolterBeatEventsCount';
import fetchHolterEcgDataMap from '../../Apollo/Functions/fetchHolterEcgDataMap';
import fetchHolterOtherEventsCount from '../../Apollo/Functions/fetchHolterOtherEventsCount';
import fetchHolterRhythmEventsCount from '../../Apollo/Functions/fetchHolterRhythmEventsCount';
import fetchHolterTasks from '../../Apollo/Functions/fetchHolterTasks';
import fetchListEcgBookmarks from '../../Apollo/Functions/fetchListEcgBookmarks';
import fetchNewReportData from '../../Apollo/Functions/fetchNewReportData';
import fetchQuerySignature from '../../Apollo/Functions/fetchQuerySignature';
import handleMarkReportAsArtifactReport from '../../Apollo/Functions/handleMarkReportAsArtifactReport';
import handleUpdateReportStatusV2 from '../../Apollo/Functions/handleUpdateReportStatusV2';
import IconButton from '../../ComponentsV2/Buttons/iconButton';
import ConfirmModal from '../../ComponentsV2/ConfirmModal';
import FacilityInfoDrawer from '../../ComponentsV2/Drawers/facilityInfoDrawer';
import PatientInfoDrawer from '../../ComponentsV2/Drawers/patientInfoDrawer';
import StudyInfoDrawer from '../../ComponentsV2/Drawers/studyInfoDrawer';
import {
// resetEcgViewerConfigStore,
ECG_VIEWER_CONFIG_STORE,
} from '../../ComponentsV2/FabricJS/Constants';
import {
CALL_CENTER_PORTAL_URL,
EMITTER_CONSTANTS,
} from '../../ConstantsV2';
import {
// OTHER_EVENT_TYPE_OBJECT,
// OTHER_EVENT_TYPES,
IS_NOT_EDIT_REPORT_MESSAGE,
} from '../../ConstantsV2/aiConstants';
import LOADING_TYPES from '../../ConstantsV2/loadingTypes';
import AiPDFReportTab from '../../LayoutV2/Reports/AiEditReport/AiPdfReport';
import BeatHR from '../../LayoutV2/Reports/AiEditReport/BeatHR';
import { combinedEventsInHeatmapState } from '../../LayoutV2/Reports/AiEditReport/BeatHR/recoil';
import {
actionSnapshotRecoilState,
ecgBookmarksState,
ecgDataMapState,
facilityNoteState,
findingsState,
isLoadedIndexDBState,
isOpenProcessingLimitState,
isShowTabHeaderState,
isStartedAiProcessingState,
originalDailySummariesState,
profileAfibAvgHrState,
reportDataState,
reportInfoState,
isUndoDisabledState,
highPassThumbnailState,
channelsThumbnailState,
gainThumbnailState,
sizeEventThumbnailConfigState,
} from '../../LayoutV2/Reports/AiEditReport/Recoil';
import {
defaultReportOptions,
reportOptionsState,
} from '../../LayoutV2/Reports/AiEditReport/AiPdfReport/recoil';
import eventEmitter from '../../UtilsV2/eventEmitter';
import RhythmEvents from '../../LayoutV2/Reports/AiEditReport/RhythmEvents';
import {
beatEventCountState,
otherEventCountState,
rhythmEventCountState,
} from '../../LayoutV2/Reports/AiEditReport/RhythmEvents/recoil';
import StripsManagement from '../../LayoutV2/Reports/AiEditReport/StripsManagement';
import {
CONFIG_ECG,
REPORT_TYPE,
logError,
} from '../../LayoutV2/Reports/AiEditReport/handler';
import { fetchHolterOtherEventsTypesRequest } from '../../ReduxV2/Actions/holterOtherEventsTypes';
import loadingPage from '../../ReduxV2/Actions/loading';
import closeIcon from '../../StaticV2/Images/Components/close-icon.svg';
import icArtifactActive from '../../StaticV2/Images/Components/ic-artifact-active.svg';
import icArtifact from '../../StaticV2/Images/Components/ic-artifact.svg';
import {
timezoneToOffset,
zeroPad,
simulateKeyEvent,
} from '../../UtilsV2';
import {
generateAiStatusReport,
generateListDate,
isHolterProcessingAvailable,
} from '../../UtilsV2/aiUtils';
import auth from '../../UtilsV2/auth';
import {
useActions,
useEmitter,
useGetRecoilValue,
useMergeState,
} from '../../UtilsV2/customHooks';
import logHandler from '../../UtilsV2/logHandler';
import socketio from '../../UtilsV2/socketio';
import {
toastrError,
toastrSuccess,
} from '../../UtilsV2/toastNotification';
import CheckSavePrompt from './CheckSavePrompt';
import ReportHeaderWrapper from './ReportHeader/reportHeaderWrapper';
import ReviewedReportIcon from './assets/reviewed-report-icon.svg';
import UnreviewedReportIcon from './assets/unreviewed-report-icon.svg';
import UndoIcon from './assets/undo-icon.svg';
import DebugObserver from './debugObserver';
import { clearAllDataStateRecoil, getDataStateRecoil, upsertDataStateRecoil } from '../../Store/dbStateRecoil';
import { resetHistoryStateRecoil } from '../../Store/dbHistoryStateRecoil';
import InitRecoilFromIndexDB from '../../LayoutV2/Reports/AiEditReport/Recoil/InitRecoilFromIndexDB';
import handleUpdateHolterProfile from '../../Apollo/Functions/handleUpdateHolterProfile';
import UndoStateRecoil from '../../LayoutV2/Reports/AiEditReport/Recoil/undoStateRecoil';
import ProgressLoading from '../../LayoutV2/Reports/AiEditReport/SharedModule/progressLoading';
import { checkHasChangedSavedIndexDB } from './handler';
// IndexDB for Beat Data and ECG Data
import {
addMultipleBeatData, addMetaBeatData, clearDBBeatData,
} from '../../Store/dbBeatData';
import {
addMultipleECGData, addMetaEcgData, clearDBEcgData,
} from '../../Store/dbECGData';
import studySummariesJson from '../../../dummyData/studySummaries.json';
// eslint-disable-next-line import/no-webpack-loader-syntax
/*
All hot keys:
N,S,V,Q,Z,X,C,W,F,ESC,B,CTRL+A,CTRL+S,D,R,A,arrow up down left right,>,<,1,2,3,4,5,6
N,S,V,Q: change beat type
D: delete beat/event
Z: previous page
X: next page
C: open change duration event
W: open add new event
F: open add bookmark
R: mark/unmark event reviewed
ESC: escape from mode
B: toggle Add Remove Beat mode (ECGViewer)
A: mark event as artifact
CTRL+A: select all strip
CTRL+S: Save data
arrow up down left right: navigate through strip in page
1,2,3,4,5,6: set bulk action beat
*/
const AiReportPage = () => {
const { studyFid } = useParams();
const history = useHistory();
const isMacOS = navigator.userAgent.indexOf('Mac') !== -1;
const [loadingPageAction, fetchHolterOtherEventsTypesAction] = useActions([loadingPage, fetchHolterOtherEventsTypesRequest]);
const defaultActiveTab = useRef('4');
const [state, setState] = useMergeState({
isLoadedData: false,
isGeneratingNormalReportInCallCenter: false,
isOpenStudyInfoDrawer: false,
isOpenPatientInfoDrawer: false,
isOpenFacilityInfoDrawer: false,
isOpenNoActivityModal: false,
isOpenConfirmArtifact: false,
isOpenConfirmNavigateTab: false,
isOpenUseDbIndexModal: false,
isLoadingSaveAiReport: false,
conflictUsers: [],
activeTab: defaultActiveTab.current,
});
const [isNotEdit, setIsNotEdit] = useState(true);
const [isLoadIndexDB, setIsLoadIndexDB] = useState(false);
const [prevEcgDataMap, setPrevEcgDataMap] = useState(null);
const [reportData, setReportData] = useRecoilState(reportDataState);
const [reportInfo, setReportInfo] = useRecoilState(reportInfoState);
const getReportInfo = useGetRecoilValue(reportInfoState);
const [ecgDataMap, setEcgDataMap] = useRecoilState(ecgDataMapState);
const isStartedAiProcessing = useRecoilValue(isStartedAiProcessingState);
const [isOpenProcessingLimit, setIsOpenProcessingLimit] = useRecoilState(isOpenProcessingLimitState);
const setFindings = useSetRecoilState(findingsState);
const setFacilityNote = useSetRecoilState(facilityNoteState);
const setOriginalDailySummaries = useSetRecoilState(originalDailySummariesState);
const setProfileAfibAvgHr = useSetRecoilState(profileAfibAvgHrState);
const isShowTabHeader = useRecoilValue(isShowTabHeaderState);
const setCombinedEventsInHeatmap = useSetRecoilState(combinedEventsInHeatmapState);
const setEcgBookmarks = useSetRecoilState(ecgBookmarksState);
const setActionSnapshotRecoil = useSetRecoilState(actionSnapshotRecoilState);
const setHolterBeatEventsCount = useSetRecoilState(beatEventCountState);
const setHolterRhythmEventsCount = useSetRecoilState(rhythmEventCountState);
const setHolterOtherEventsCount = useSetRecoilState(otherEventCountState);
const setHighPassThumbnail = useSetRecoilState(highPassThumbnailState);
const setSizeEventThumbnailConfig = useSetRecoilState(sizeEventThumbnailConfigState);
const setGainThumbnail = useSetRecoilState(gainThumbnailState);
const setChannelsThumbnail = useSetRecoilState(channelsThumbnailState);
const setReportOptions = useSetRecoilState(reportOptionsState);
const isLoadedIndexDB = useRecoilValue(isLoadedIndexDBState);
const [isUndoDisabled, setIsUndoDisabled] = useRecoilState(isUndoDisabledState);
useEffect(() => {
// Set the default value to true
setTimeout(() => {
setIsUndoDisabled(true);
}, 100);
const updateIndex = (currentIndex) => {
setIsUndoDisabled(currentIndex < 1);
};
eventEmitter.addListener(EMITTER_CONSTANTS.UNDO_REDO_CURRENT_INDEX, updateIndex);
// Clean up the event listener on component unmount
return () => {
// eventEmitter.removeListener(EMITTER_CONSTANTS.UNDO_REDO_CURRENT_INDEX, updateIndex);
};
}, []);
const {
studyId,
profileId,
facilityId,
reportId,
isAbortedStudy,
startReport,
timezoneOffset,
} = reportInfo;
const roomReport = useRef();
const updatingTabCount = useRef(0);
const tabIndexConfirmNavigate = useRef();
const tab1Ref = useRef();
const tab2Ref = useRef();
const tab3Ref = useRef();
const tab4Ref = useRef();
const findingRef = useRef();
const ecgDisclosureRef = useRef();
const sessionInfoRef = useRef();
const fetchEventsCount = async () => {
try {
const eventsCountFilter = {
studyId,
profileId,
};
const promises = [
fetchHolterBeatEventsCount(eventsCountFilter, false, null, false),
fetchHolterRhythmEventsCount(eventsCountFilter, false, null, false),
fetchHolterOtherEventsCount(eventsCountFilter, false, null, false),
];
const [
holterBeatEventsCount,
holterRhythmEventsCount,
holterOtherEventsCount,
] = await Promise.all(promises);
setHolterBeatEventsCount(holterBeatEventsCount);
setHolterRhythmEventsCount(holterRhythmEventsCount);
setHolterOtherEventsCount(holterOtherEventsCount);
} catch (error) {
logError('Failed to fetch holter rhythm events count: ', error);
}
};
const getDatabaseStateRecoil = async () => {
const activeTab = await getDataStateRecoil('activeTab');
if (activeTab?.value) {
setState({ activeTab: activeTab?.value });
defaultActiveTab.current = activeTab?.value;
}
};
const restDefaultState = () => {
setHighPassThumbnail(1);
setGainThumbnail(7.5);
setChannelsThumbnail([4]);
setSizeEventThumbnailConfig({ name: 'Small', width: 415, height: 87.1 });
setReportOptions(defaultReportOptions);
upsertDataStateRecoil('hourlyBeatDataArray', []);
};
// TODO : handle clear all beat and ecg data
const clearAllBeatData = async () => {
await clearDBEcgData();
await clearDBBeatData();
};
const clearDataAndResaveSessionInfo = async () => {
await clearAllDataStateRecoil();
clearAllBeatData();
setFindings(findingRef.current);
if (!isNotEdit && sessionInfoRef.current) {
upsertDataStateRecoil('sessionInfo', sessionInfoRef.current);
upsertDataStateRecoil('activeTab', state.activeTab);
}
};
const handleClearIndexDB = async (info) => {
try {
const dataSave = await getDataStateRecoil('sessionInfo');
if (dataSave?.value) {
const { studyId, profileId, sessionTime } = info;
if (dataSave?.value?.studyId !== studyId
|| dataSave?.value?.profileId !== profileId
|| dayjs(dataSave?.value?.sessionTime).valueOf() !== dayjs(sessionTime).valueOf()) {
// clean all data
clearDataAndResaveSessionInfo();
setIsLoadIndexDB(true);
} else {
// show popup when stay in tab 1 or tab 2
const activeTab = await getDataStateRecoil('activeTab');
// check has change data
const isChanged = await checkHasChangedSavedIndexDB({
activeTab: activeTab?.value,
currentFinding: findingRef.current,
currentEcgDisclosure: ecgDisclosureRef.current,
});
if (isChanged) {
setState({ isOpenUseDbIndexModal: true });
} else {
clearDataAndResaveSessionInfo();
setIsLoadIndexDB(true);
}
}
} else {
setIsLoadIndexDB(true);
}
} catch (error) {
console.error(error);
}
};
const onDiscardChangeSavedData = () => {
setState({ isOpenUseDbIndexModal: false });
clearDataAndResaveSessionInfo();
setIsLoadIndexDB(true);
};
const onKeepEditingWithSavedData = () => {
setState({ isOpenUseDbIndexModal: false });
setIsLoadIndexDB(true);
};
const fetchReportData = async (studyFid) => {
try {
const input = {
studyFid: studyFid ? parseInt(studyFid, 10) : undefined,
type: REPORT_TYPE,
};
const aiReportData = await fetchNewReportData({ input });
const { reportData, isSuccess } = aiReportData;
if (isSuccess && !_.isEmpty(reportData)) {
if (reportData.study?.isArchived || generateAiStatusReport(reportData.study) !== 'Ready') {
history.replace('/404');
return;
}
const studyId = reportData.study?.id;
const profileId = reportData.study?.holterProfile?.id;
const [downloadEcgDataMap, holterTasks, studyReportsData] = await Promise.all([
fetchHolterEcgDataMap({ studyId, profileId }),
fetchHolterTasks(),
fetchNewReportData({ id: studyId, isStudyReport: true }),
]);
if (downloadEcgDataMap) {
CONFIG_ECG.samplingFrequency = downloadEcgDataMap.samplingFrequency;
CONFIG_ECG.gain = downloadEcgDataMap.gain;
ECG_VIEWER_CONFIG_STORE.visibleChannels = _.map(downloadEcgDataMap.channels, item => Number(item.slice(-1)) - 1);
ECG_VIEWER_CONFIG_STORE.originalChannels = downloadEcgDataMap.channels;
}
const studyReport = _.filter(studyReportsData, x => x.type === 'Study')[0];
const aiReport = _.find(studyReportsData, x => x.type === 'AI EOU');
socketio.emitRoom(EMITTER_CONSTANTS.HOLTER_GENERAL_ROOM);
socketio.emitRoom(EMITTER_CONSTANTS.HOLTER_PROFILE_ROOM.replace('{profileId}', profileId));
socketio.emitRoom(reportData.study?.device?.deviceId);
// *: Join room report
if (reportData?.report?.type === 'Study' || reportData.report?.type === 'AI EOU' || reportData?.report?.reportId) {
const room = `${zeroPad(studyFid)}_ai_eou`;
roomReport.current = room;
socketio.sendJoinReportRoom(roomReport.current);
}
// isHolterReport: [STUDY_TYPE.HOLTER, STUDY_TYPE.EXTENDED_HOLTER, STUDY_TYPE.MCT_PEEK].includes(reportData.study?.studyType)
const startReport = reportData?.report?.start;
const stopReport = reportData?.report?.stop;
const timezone = reportData.study?.timezone;
const timezoneOffset = timezoneToOffset(timezone);
const listDate = generateListDate(startReport, stopReport, timezoneOffset);
const combinedEventsInHeatmap = {};
_.forEach(listDate, (date) => {
combinedEventsInHeatmap[date] = [];
});
setCombinedEventsInHeatmap(combinedEventsInHeatmap);
const info = {
studyId: reportData.study?.id,
profileId: reportData.study?.holterProfile?.id,
sessionTime: reportData.study?.holterProfile?.sessionTime,
facilityId: reportData.study?.facility?.id,
reportId: reportData.report?.id,
reportFid: reportData.report?.reportId,
isAbortedStudy: reportData.study?.status === 'Aborted',
studyFid: reportData.study?.friendlyId,
patientName: `${reportData.study?.info?.patient?.firstName} ${reportData.study?.info?.patient?.lastName}`,
facilityName: reportData.study?.facility?.name,
reportType: reportData.report?.type,
timezone,
timezoneOffset,
startReport,
stopReport,
};
setReportInfo(info);
// *: Sort data by start time
downloadEcgDataMap.data.sort((a, b) => a.start - b.start);
for (let i = 0; i < downloadEcgDataMap.data.length; i += 1) {
const item = downloadEcgDataMap.data[i];
item.start = dayjs(item.start);
item.stop = dayjs(item.stop);
}
findingRef.current = reportData?.report?.technicianComments || reportData?.study?.holterProfile?.technicianComments;
ecgDisclosureRef.current = {
ecgDisclosure: reportData.study?.holterProfile?.ecgDisclosure,
timezoneOffset,
ecgDataMap: downloadEcgDataMap,
};
// check clean IndexDB
handleClearIndexDB(info);
logHandler.setStudyId(studyId);
setProfileAfibAvgHr(reportData.study?.holterProfile?.afibAvgHr);
setOriginalDailySummaries(reportData.study?.holterProfile?.mctDailySummaries);
setEcgDataMap(downloadEcgDataMap);
// setFindings(reportData?.report?.technicianComments || reportData?.study?.holterProfile?.technicianComments);
setFacilityNote(reportData?.study?.facility?.facilityNote);
setReportData(reportData);
setIsOpenProcessingLimit(!isHolterProcessingAvailable(holterTasks, reportData?.study?.friendlyId));
setState({
isLoadedData: true,
isGeneratingNormalReportInCallCenter: studyReport?.status === 'Rendering',
studyReport,
aiReport,
});
} else {
history.replace('/404');
}
} catch (error) {
logError('Failed to load report data ', error);
history.push('/404');
}
loadingPageAction();
};
const toggleDisplayStudyInfoDrawer = () => {
setState({ isOpenStudyInfoDrawer: !state.isOpenStudyInfoDrawer });
};
const toggleDisplayPatientInfoDrawer = () => {
setState({ isOpenPatientInfoDrawer: !state.isOpenPatientInfoDrawer });
};
const toggleDisplayFacilityInfoDrawer = () => {
setState({ isOpenFacilityInfoDrawer: !state.isOpenFacilityInfoDrawer });
};
const onClickReload = () => {
// *Cannot use window.location.reload() because it not work in Firefox
// window.location.reload();
window.location.href = window.location.href;
};
const handleOnClickArtifactReport = async (flag) => {
try {
await handleMarkReportAsArtifactReport(reportId, flag);
} catch (err) {
toastrError(err, t`Error`);
}
setState({ isOpenConfirmArtifact: false });
};
const navigateTab = (tab) => {
if (tab !== state.activeTab) {
if (isNotEdit) {
setState({ activeTab: tab || defaultActiveTab.current });
return;
}
// check data changed
const tabRef = {
1: tab1Ref.current,
2: tab2Ref.current,
3: tab3Ref.current,
4: tab4Ref.current,
}[state.activeTab];
const isChangedData = tabRef ? tabRef.isChangedData() : false;
console.log('[aiReport]-navigateTab', isChangedData, tab);
if (isChangedData) {
tabIndexConfirmNavigate.current = tab;
setState({ isOpenConfirmNavigateTab: true });
} else {
//* Do not reset config
// resetEcgViewerConfigStore();
tabIndexConfirmNavigate.current = undefined;
setState({ activeTab: tab || defaultActiveTab.current, isOpenConfirmNavigateTab: false });
setActionSnapshotRecoil(1);
// reset undo/redo related components
restDefaultState();
setTimeout(() => {
resetHistoryStateRecoil();
}, 500);
}
}
};
const onClickCancelNavigateTab = () => {
tabIndexConfirmNavigate.current = undefined;
setState({ isOpenConfirmNavigateTab: false });
};
const onClickNavigateWithoutSave = () => {
const tabRef = {
1: tab1Ref.current,
2: tab2Ref.current,
3: tab3Ref.current,
4: tab4Ref.current,
}[state.activeTab];
if (tabRef) {
tabRef.resetData();
}
const tabNavigate = tabIndexConfirmNavigate.current;
// reset undo/redo related components
restDefaultState();
setTimeout(() => {
resetHistoryStateRecoil();
}, 500);
setTimeout(() => {
navigateTab(tabNavigate);
}, 20);
};
const onClickConfirmNavigateTab = () => {
const tabRef = {
1: tab1Ref.current,
2: tab2Ref.current,
3: tab3Ref.current,
4: tab4Ref.current,
}[state.activeTab];
if (tabRef) {
tabRef.onClickSaveReport();
}
// reset undo/redo related components
restDefaultState();
setTimeout(() => {
resetHistoryStateRecoil();
}, 500);
};
const saveReportOnClick = () => {
const tabRef = {
1: tab1Ref.current,
2: tab2Ref.current,
3: tab3Ref.current,
4: tab4Ref.current,
}[state.activeTab];
if (tabRef) {
tabRef.onClickSaveReport();
}
// reset undo/redo related components
restDefaultState();
setTimeout(() => {
resetHistoryStateRecoil();
}, 500);
};
const handleNoActivity = () => {
if (window.isStopAiProcess) {
return;
}
socketio.sendLeaveAllRooms();
setState({ isOpenNoActivityModal: true });
};
const checkDataChange = () => {
const result = (state.isOpenNoActivityModal ? false : (
tab1Ref.current?.isChangedData?.()
|| tab2Ref.current?.isChangedData?.()
|| tab3Ref.current?.isChangedData?.()
|| tab4Ref.current?.isChangedData?.()
));
console.log('[aiReport]-checkStudyReportDataChange', result, state.isOpenNoActivityModal);
return result;
};
const renderMarkArtifact = () => (
<div id="bt-artifact">
<IconButton
disabled={
reportData?.study?.holterProfile?.isArtifactReport
|| isNotEdit
}
iconComponent={(
<img
src={
(reportData?.study?.holterProfile?.isArtifactReport || reportData?.report?.isArtifactReport)
? icArtifactActive
: icArtifact
}
alt={t`Artifact icon`}
/>
)}
onClick={() => {
if (!reportData?.report?.isArtifactReport) {
setState({ isOpenConfirmArtifact: true });
} else {
handleOnClickArtifactReport(!reportData?.report?.isArtifactReport);
}
}}
/>
<UncontrolledTooltip
target="bt-artifact"
className="custom-tooltip"
placement="bottom"
delay={{ show: 0, hide: 0 }}
hideArrow
>
{(() => {
if (reportData?.study?.holterProfile?.isArtifactReport) return t`Artifact report due to no beats and events detected`;
if (isNotEdit) return IS_NOT_EDIT_REPORT_MESSAGE;
return reportData?.report?.isArtifactReport ? t`Unmark report as artifact` : t`Mark report as artifact`;
})()}
</UncontrolledTooltip>
</div>
);
const handleMarkReviewedClick = async () => {
if (_.isEmpty(state.studyReport) && _.isEmpty(state.aiReport)) {
return;
}
const isReviewedAiReport = state.aiReport.status === 'Reviewed';
try {
const status = state.aiReport.status === 'Reviewed'
? 'Ready'
: 'Reviewed';
const aiReportFilter = {
id: state.aiReport.id,
status,
};
let reportFilter;
const promises = [handleUpdateReportStatusV2(aiReportFilter)];
if (state.studyReport) {
reportFilter = {
id: state.studyReport.id,
status,
};
promises.push(handleUpdateReportStatusV2(reportFilter));
}
await Promise.all(promises);
setState({
studyReport: state.studyReport
? {
...state.studyReport,
status: reportFilter.status,
}
: null,
aiReport: {
...state.aiReport,
status: aiReportFilter.status,
},
});
toastrSuccess(
isReviewedAiReport
? t`Mark as unreviewed`
: t`Mark as reviewed`,
t`Success`,
);
} catch (error) {
console.error('Failed to mark reviewed', error);
toastrError(
isReviewedAiReport
? t`Failed to mark as unreviewed`
: t`Failed to mark as reviewed`,
t`Error`,
);
}
};
const renderMarkReviewed = () => {
// const isReviewedReport = state.studyReport?.status === 'Reviewed';
const isReviewedAiReport = state.aiReport?.status === 'Reviewed';
return (
<div id="bt-mark-reviewed">
<IconButton
iconComponent={(
<img
src={
isReviewedAiReport
? ReviewedReportIcon
: UnreviewedReportIcon
}
alt={t`Artifact icon`}
/>
)}
MarkAsReviewedIcon
onClick={handleMarkReviewedClick}
/>
<UncontrolledTooltip
target="bt-mark-reviewed"
className="custom-tooltip"
placement="bottom"
delay={{ show: 0, hide: 0 }}
hideArrow
>
{
isReviewedAiReport
? t`Mark as unreviewed`
: t`Mark as reviewed`
}
</UncontrolledTooltip>
</div>
);
};
const renderUndoState = () => (
<div id="bt-undo-btn">
<IconButton
className="more-icon mr-3"
isOutline
disabled={isUndoDisabled}
iconComponent={(
<img src={UndoIcon} alt={t`Undo icon`} />
)}
onClick={() => { simulateKeyEvent('Z', { ctrlKey: true }); }}
/>
<UncontrolledTooltip
target="bt-undo-btn"
className="custom-tooltip"
placement="bottom"
delay={{ show: 0, hide: 0 }}
hideArrow
>
{/* TODO: currently set disabled is true */}
{isUndoDisabled ? t`There is nothing to undo` : (isMacOS ? t`Undo ⌘ Z` : t`Undo Ctrl Z`)}
</UncontrolledTooltip>
</div>
);
const handleFetchBookmarks = async () => {
try {
const ecgBookmarks = await fetchListEcgBookmarks({
studyId,
sortOrder: 'asc',
skip: 0,
}, 0);
setEcgBookmarks(ecgBookmarks);
} catch (error) {
logError('Failed to fetch bookmarks ', error);
}
};
useEffect(() => {
if (isStartedAiProcessing) {
fetchEventsCount();
handleFetchBookmarks();
}
}, [isStartedAiProcessing]);
useEffect(() => {
let interval;
//* Refresh query signature every 1 hour
if (state.isLoadedData) {
const fetchNewQuerySignature = async () => {
try {
const querySignature = await fetchQuerySignature({ studyId });
startTransition(() => {
setEcgDataMap(currentEcgDataMap => ({ ...currentEcgDataMap, querySignature }));
});
} catch (error) {
console.log(error);
}
};
interval = setInterval(() => {
fetchNewQuerySignature();
}, 3600000); // 1 hour
}
return () => {
try {
if (interval) {
clearInterval(interval);
}
} catch (error) {
console.log(error);
}
};
}, [state.isLoadedData, studyId]);
useEffect(() => {
if (Number(studyFid)) {
// get database local
getDatabaseStateRecoil();
fetchReportData(studyFid);
// TODO: Fetch other holter events types
// handleFetchHolterOtherEventsTypes();
// fetchHolterOtherEventsTypesAction();
} else {
history.replace('/404');
}
}, [studyFid]);
useEffect(() => {
upsertDataStateRecoil('activeTab', state.activeTab);
}, [state.activeTab]);
useEffect(() => {
if (!isNotEdit) {
const info = getReportInfo();
const sessionTime = dayjs();
// update session time
const sessionInfo = {
sessionTime: sessionTime.toISOString(),
studyId: info.studyId,
profileId: info.profileId,
};
const updateHolterProfileInput = {
id: info.profileId,
sessionTime,
};
sessionInfoRef.current = sessionInfo;
handleUpdateHolterProfile(updateHolterProfileInput);
upsertDataStateRecoil('sessionInfo', sessionInfo);
}
}, [isNotEdit]);
useEmitter(EMITTER_CONSTANTS.REPORT_UPDATED, (msg) => {
const reportDataClone = _.cloneDeep(reportData);
if (msg.studyFid === reportDataClone.study?.friendlyId) {
if (msg.id === reportDataClone.report?.id) {
_.assign(reportDataClone.report, msg);
setReportData(reportDataClone);
} else {
// Report format cũ ở bên callcenter nếu đang generate sẽ chặn ko cho copy report AI
setState({ isGeneratingNormalReportInCallCenter: msg.status === 'Rendering' });
}
}
}, [reportData]);
useEmitter(EMITTER_CONSTANTS.REPORT_ROOM_USER, async (msg) => {
const updatedEvent = _.findLast(msg, x => x.room === roomReport.current);
if (updatedEvent) {
const { users } = updatedEvent;
const accessToken = auth.getOriginalToken();
// *: Check user can edit report
let newIsNotEdit = true;
const sortedConflictUsers = users?.sort((a, b) => (a.time - b.time));
const firstUser = sortedConflictUsers?.length > 0 ? sortedConflictUsers[0] : {};
if (!_.isEmpty(firstUser.userId) && !_.isEmpty(firstUser.token)) {
if (firstUser.userId === auth.userId() && firstUser.token === (accessToken || '').slice(-10)) {
newIsNotEdit = false;
} else {
newIsNotEdit = true;
}
} else {
newIsNotEdit = true;
}
console.log('[aiReport]-SOCKETROOMUSER', users, accessToken, firstUser, newIsNotEdit);
//* Reload 2nd user when 1st user exit
if (isNotEdit && !newIsNotEdit && isStartedAiProcessing && state.conflictUsers.length > 1 && users.length === 1) {
logHandler.addLog('reload', {
users, conflictUsers: state.conflictUsers, isNotEdit, newIsNotEdit,
});
await logHandler.sendLog();
onClickReload();
return;
}
setIsNotEdit(newIsNotEdit);
setState({ conflictUsers: sortedConflictUsers });
}
}, [isNotEdit, state.conflictUsers, isStartedAiProcessing]);
useEmitter(EMITTER_CONSTANTS.HOLTER_PROFILE_UPDATED, (msg) => {
const reportDataClone = _.cloneDeep(reportData);
if (msg?.ecgDisclosure) {
_.assign(reportDataClone.study?.holterProfile, { ecgDisclosure: msg.ecgDisclosure });
}
if (!_.isEmpty(msg?.reportConfiguration)) {
_.assign(reportDataClone.study?.holterProfile, { reportConfiguration: msg.reportConfiguration });
}
setReportData(reportDataClone);
}, [reportData]);
useEmitter(EMITTER_CONSTANTS.AI_UPDATE_TAB, (msg) => {
updatingTabCount.current += 1;
console.log('[aiReport]-AI_UPDATE_TAB', updatingTabCount.current);
}, []);
useEmitter(EMITTER_CONSTANTS.AI_LOADING, (msg) => {
// {
// isLoading: boolean,
// }
const { isLoading } = msg || {};
console.log('[aiReport]-AI_LOADING', msg, updatingTabCount.current, tabIndexConfirmNavigate.current, state.isOpenConfirmNavigateTab);
if (isLoading) {
updatingTabCount.current = 0;
setState({ isLoadingSaveAiReport: true });
} else if (updatingTabCount.current > 0) {
updatingTabCount.current -= 1;
if (updatingTabCount.current === 0) {
setState({ isLoadingSaveAiReport: false });
const tabNavigate = tabIndexConfirmNavigate.current;
if (state.isOpenConfirmNavigateTab && tabNavigate) {
setTimeout(() => {
navigateTab(tabNavigate);
}, 20);
}
}
} else {
updatingTabCount.current = 0;
setState({ isLoadingSaveAiReport: false });
const tabNavigate = tabIndexConfirmNavigate.current;
if (state.isOpenConfirmNavigateTab && tabNavigate) {
setTimeout(() => {
navigateTab(tabNavigate);
}, 20);
}
}
}, [state.isOpenConfirmNavigateTab]);
useEmitter(EMITTER_CONSTANTS.COMMAND_PROGRESS_UPDATED, (msg) => {
const {
command, doneTasks, totalTasks, percentage,
} = msg;
if (command === 'update-report-data') {
loadingPageAction(`${LOADING_TYPES.GENERATE_REPORT} ${percentage} %`);
}
}, []);
useEmitter(EMITTER_CONSTANTS.EVENTSUPDATED_EVENT, (msg) => {
if (isStartedAiProcessing) {
fetchEventsCount();
}
}, [isStartedAiProcessing]);
// Generic function to group data by epoch start of the day
const groupDataByEpochStartOfDay = (data, dateKey) => data.reduce((acc, item) => {
const dateEpoch = dayjs(item[dateKey]).startOf('day').valueOf();
if (!acc[dateEpoch]) {
acc[dateEpoch] = [];
}
acc[dateEpoch].push(item);
return acc;
}, {});
const handleDownloadBeatList = async () => {
let thread;
try {
// Initialize worker and spawn a thread
const worker = new Worker(beatWorker);
thread = await spawn(worker);
// Configure the thread with necessary parameters
await thread.createConfig({
samplingFrequency: ecgDataMap.samplingFrequency,
querySignature: ecgDataMap.querySignature,
});
// Define how the worker should handle incoming messages
worker.onmessage = async (message) => {
const { type, data } = message.data;
if (type === 'beatData' || type === 'ecgData') {
const { result, metadata } = data;
console.log(`[beatWorker] ${type}`, result.length, metadata);
if (metadata.failedLinks && metadata.failedLinks.length > 0) {
metadata.failedLinks.forEach((link) => {
console.error(`Failed to download file ${type} with ID: ${link.id}, Error: ${link.error}`);
});
}
if (type === 'beatData') {
await addMultipleBeatData(result);
await addMetaBeatData(metadata.dateEpoch, metadata);
} else if (type === 'ecgData') {
await addMultipleECGData(result);
await addMetaEcgData(metadata.dateEpoch, metadata);
}
}
};
// Group data by epoch start of the day
const groupedDataBeat = groupDataByEpochStartOfDay(studySummariesJson, 'start');
const groupedDataECG = groupDataByEpochStartOfDay(ecgDataMap.data, 'start');
// Process beat data in worker
for (const dateEpoch of Object.keys(groupedDataBeat)) {
const data = groupedDataBeat[dateEpoch];
await thread.downloadBeatListByDay(dateEpoch, data);
}
// Process ECG data in worker
for (const dateEpoch of Object.keys(groupedDataECG)) {
const data = groupedDataECG[dateEpoch];
await thread.downloadECGListByDay(dateEpoch, data);
}
} catch (error) {
console.error('Failed to download beat list:', error);
} finally {
// Terminate the worker thread
if (thread) {
// Thread.terminate(thread).catch(console.error);
}
}
};
useEffect(() => {
if (
ecgDataMap
&& Object.keys(ecgDataMap).length > 0
&& !_.isEqual(ecgDataMap.data.length, prevEcgDataMap?.data.length)
) {
handleDownloadBeatList();
setPrevEcgDataMap(ecgDataMap);
}
}, [ecgDataMap]);
return (
<>
<CheckSavePrompt
isNotEdit={isNotEdit}
condition={checkDataChange}
/>
<StudyInfoDrawer
visible={state.isOpenStudyInfoDrawer}
title={t`Study Information`}
fetchStudyId={studyId}
handleClickCloseButton={toggleDisplayStudyInfoDrawer}
/>
<PatientInfoDrawer
visible={state.isOpenPatientInfoDrawer}
title={t`Patient Information`}
studyId={studyId}
handleClickCloseButton={toggleDisplayPatientInfoDrawer}
/>
<FacilityInfoDrawer
visible={state.isOpenFacilityInfoDrawer}
title={t`Facility Information`}
facilityId={facilityId}
handleClickCloseButton={toggleDisplayFacilityInfoDrawer}
isAbortedStudy={isAbortedStudy}
/>
<ConfirmModal
isOpen={isOpenProcessingLimit}
onClickRightButton={() => { document.location.href = CALL_CENTER_PORTAL_URL; }}
rightButtonName={t`Okay`}
title={t`Approaching processing limit`}
question={t`The current limit for viewing and processing reports by AI has been exceeded.
Please try again later.`}
isRedBtn={false}
/>
<ConfirmModal
isOpen={state.isOpenNoActivityModal}
onClickRightButton={onClickReload}
rightButtonName={t`Reload The Page`}
title={t`Notification`}
question={t`Since there has been no activity for the past 30 minutes, please reload the page to resume.`}
isRedBtn={false}
/>
<ConfirmModal
isOpen={state.isOpenConfirmArtifact}
onClickLeftButton={() => { setState({ isOpenConfirmArtifact: false }); }}
onClickRightButton={() => handleOnClickArtifactReport(true)}
leftButtonName={t`No`}
rightButtonName={t`Yes`}
title={t`Mark report as artifact`}
question={t`Are you sure you want to mark this report as an "Artifact report"?`}
/>
<ConfirmModal
isOpen={state.isOpenUseDbIndexModal}
className="unsaved-change-modal --use-db-index"
onClickLeftButton={onDiscardChangeSavedData}
onClickRightButton={onKeepEditingWithSavedData}
leftButtonName={t`Discard changes`}
rightButtonName={t`Keep editing`}
title={t`Unsaved changes`}
question={t`There are unsaved changes. If you would like to keep changes, press the “Keep editing” button below.`}
/>
<ConfirmModal
isOpen={state.isOpenConfirmNavigateTab}
className="unsaved-change-modal"
onClickLeftButton={onClickNavigateWithoutSave}
onClickRightButton={onClickConfirmNavigateTab}
leftButtonName={state.isLoadingSaveAiReport ? null : t`Don’t Save`}
rightButtonName={t`Save Changes`}
isFilledRightBtn
isBoldLeftBtn
title={(
<div className="title-unsaved-change">
<div>{t`Unsaved changes`}</div>
<IconButton
iconComponent={(
<img
src={closeIcon}
alt={t`Close icon`}
/>
)}
onClick={onClickCancelNavigateTab}
/>
</div>
)}
question={t`You're about to leave this tab with unsaved changes. What would you like to do before proceeding?`}
isRedBtn={false}
isSaving={state.isLoadingSaveAiReport}
/>
<div
className={classnames('custom-page')}
>
<ReportHeaderWrapper
isLoadedData={state.isLoadedData}
title={t`End of Use report`}
conflictUsers={state.conflictUsers}
toggleDisplayStudyInfoDrawer={toggleDisplayStudyInfoDrawer}
toggleDisplayPatientInfoDrawer={toggleDisplayPatientInfoDrawer}
toggleDisplayFacilityInfoDrawer={toggleDisplayFacilityInfoDrawer}
markArtifactComponent={renderMarkArtifact()}
markReviewedComponent={renderMarkReviewed()}
undoStateComponent={renderUndoState()}
isDisableSaveReport={isNotEdit || isAbortedStudy}
onClickSaveReport={saveReportOnClick}
isLoadingSaveAiReport={state.isLoadingSaveAiReport}
isNotEdit={isNotEdit}
profileId={profileId}
studyId={studyId}
isArtifactReport={reportData?.report?.isArtifactReport}
activeTab={state.activeTab}
navigateTab={navigateTab}
isStartedAiProcessing={isStartedAiProcessing}
/>
{
state.isLoadedData && (
<>
{
isLoadIndexDB && (
<InitRecoilFromIndexDB />
)
}
{/* component undo and redo */}
<UndoStateRecoil />
<DebugObserver tab1Ref={tab1Ref} tab2Ref={tab2Ref} tab3Ref={tab3Ref} tab4Ref={tab4Ref} />
<TabContent
className={classnames('study-report-top-tab-content', isShowTabHeader ? '' : '--hide-tab')}
activeTab={state.activeTab}
>
<TabPane tabId="1">
<BeatHR
tabRef={tab1Ref}
handleNoActivity={handleNoActivity}
activeTab={state.activeTab}
/>
</TabPane>
<TabPane tabId="2">
<RhythmEvents
tabRef={tab2Ref}
handleNoActivity={handleNoActivity}
activeTab={state.activeTab}
/>
</TabPane>
<TabPane tabId="3">
{isLoadedIndexDB ? (
<StripsManagement
ref={tab3Ref}
isActive={state.activeTab === '3'}
ecgDataMap={ecgDataMap}
reportData={reportData}
studyFid={parseInt(studyFid, 10)}
timezoneOffset={timezoneOffset}
startReport={startReport}
isNotEdit={isNotEdit}
reportType={REPORT_TYPE}
/>
) : <ProgressLoading />}
</TabPane>
<TabPane tabId="4">
{isLoadedIndexDB ? (
<AiPDFReportTab
ref={tab4Ref}
isActive={state.activeTab === '4'}
isAbortedStudy={isAbortedStudy}
isNotEdit={isNotEdit}
isGeneratingNormalReportInCallCenter={state.isGeneratingNormalReportInCallCenter}
/>
) : <ProgressLoading />}
</TabPane>
</TabContent>
</>
)
}
</div>
</>
);
};
AiReportPage.propTypes = {
};
export default AiReportPage;