Compare commits
1 Commits
prod-phkg
...
9239da35f8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9239da35f8 |
@@ -9,7 +9,7 @@ import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } fro
|
|||||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||||
|
|
||||||
import './CustomizableViewportOverlay.css';
|
import './CustomizableViewportOverlay.css';
|
||||||
import { studyDataForOverlayItem, instanceDataForMriOverlayItem } from './studyDataForOverlayItem';
|
import { studyDataForOverlayItem } from './studyDataForOverlayItem';
|
||||||
|
|
||||||
const EPSILON = 1e-4;
|
const EPSILON = 1e-4;
|
||||||
|
|
||||||
@@ -58,10 +58,10 @@ const studyDateItem = {
|
|||||||
title: 'Study date and time',
|
title: 'Study date and time',
|
||||||
condition: ({ referenceInstance, studyApiData }) =>
|
condition: ({ referenceInstance, studyApiData }) =>
|
||||||
referenceInstance?.StudyDate || studyApiData?.studyTime,
|
referenceInstance?.StudyDate || studyApiData?.studyTime,
|
||||||
contentF: ({ referenceInstance, formatters: { formatDate, formatTime }, studyApiData }) => {
|
contentF: ({ referenceInstance, formatters: { formatDate }, studyApiData }) => {
|
||||||
const date = referenceInstance?.StudyDate ? formatDate(referenceInstance.StudyDate) : '';
|
const date = referenceInstance?.StudyDate ? formatDate(referenceInstance.StudyDate) : '';
|
||||||
const time = studyApiData?.studyTime ? formatTime(studyApiData.studyTime) : '';
|
const time = studyApiData?.studyTime ? studyApiData.studyTime : '';
|
||||||
return `${date} ${time}`.trim();
|
return `${date} ${time}`;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,230 +125,12 @@ const accessionNumberItem = {
|
|||||||
contentF: ({ studyApiData }) => studyApiData.accessionNumber,
|
contentF: ({ studyApiData }) => studyApiData.accessionNumber,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tambahan untuk Overlay MRI di Top Left
|
|
||||||
const sliceThicknessItem = {
|
|
||||||
id: 'SliceThickness',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Slice Thickness: ',
|
|
||||||
title: 'Slice Thickness',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.sliceThickness,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.sliceThickness} mm`,
|
|
||||||
};
|
|
||||||
const sliceLocationItem = {
|
|
||||||
id: 'SliceLocation',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Slice Loc: ',
|
|
||||||
title: 'Slice Location',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.sliceLocation,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.sliceLocation} mm`,
|
|
||||||
};
|
|
||||||
const sliceSpacingItem = {
|
|
||||||
id: 'SliceSpacing',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Slice Spacing: ',
|
|
||||||
title: 'Slice Spacing',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.spacingBetweenSlices,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.spacingBetweenSlices} mm`,
|
|
||||||
};
|
|
||||||
const phaseFieldOfViewItem = {
|
|
||||||
id: 'PhaseFOV',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: '% Phase FOV: ',
|
|
||||||
title: 'Percent Phase FOV',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.percentPhaseFieldOfView,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.percentPhaseFieldOfView} %`,
|
|
||||||
};
|
|
||||||
const phaseFieldOfViewDimensionsItem = {
|
|
||||||
id: 'FOVDimensions',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'FOV Dim: ',
|
|
||||||
title: 'Field of View Dimensions',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.fieldOfViewDimensions,
|
|
||||||
contentF: ({ mriInstanceData }) => mriInstanceData?.fieldOfViewDimensions.join(' x '),
|
|
||||||
};
|
|
||||||
const acquisitionMatrixItem = {
|
|
||||||
id: 'AcquisitionMatrix',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Acq Matrix: ',
|
|
||||||
title: 'Acquisition Matrix',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.acquisitionMatrix,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.acquisitionMatrix.join(' x ')}`,
|
|
||||||
};
|
|
||||||
const imageMatrixItem = {
|
|
||||||
id: 'ImageMatrix',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Image Matrix (RxC): ',
|
|
||||||
title: 'Image Matrix',
|
|
||||||
condition: ({ mriInstanceData }) =>
|
|
||||||
mriInstanceData?.imageMatrixRows && mriInstanceData?.imageMatrixColumns,
|
|
||||||
contentF: ({ mriInstanceData }) =>
|
|
||||||
`${mriInstanceData.imageMatrixRows} x ${mriInstanceData.imageMatrixColumns}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mriTopLeftItems = {
|
|
||||||
id: 'cornerstoneOverlayTopLeft',
|
|
||||||
items: [
|
|
||||||
sliceThicknessItem,
|
|
||||||
sliceLocationItem,
|
|
||||||
sliceSpacingItem,
|
|
||||||
phaseFieldOfViewItem,
|
|
||||||
phaseFieldOfViewDimensionsItem,
|
|
||||||
acquisitionMatrixItem,
|
|
||||||
imageMatrixItem,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tambahan bottom left overlay MRI
|
|
||||||
const scanningSequenceItem = {
|
|
||||||
id: 'MRIScanningSequence',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Scanning Seq: ',
|
|
||||||
title: 'MRI Scanning Sequence',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.scanningSequence,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.scanningSequence} `,
|
|
||||||
};
|
|
||||||
const repetitionEchoInversionTimesItem = {
|
|
||||||
id: 'MRIRepetitionEchoInversionTimes',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'TR/TE/TI: ',
|
|
||||||
title: 'MRI Repetition/Echo/Inversion Times',
|
|
||||||
condition: ({ mriInstanceData }) =>
|
|
||||||
mriInstanceData?.repetitionTime || mriInstanceData?.echoTime || mriInstanceData?.inversionTime,
|
|
||||||
contentF: ({ mriInstanceData }) => {
|
|
||||||
const tr = mriInstanceData?.repetitionTime || '-';
|
|
||||||
const te = mriInstanceData?.echoTime || '-';
|
|
||||||
const ti = mriInstanceData?.inversionTime || '-';
|
|
||||||
return `${tr}/${te}/${ti} ms`.replace(/\/+/g, ' / ').trim(); // Clean up extra slashes
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const receiverCoilItem = {
|
|
||||||
id: 'MRIReceiverCoil',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Receiver Coil: ',
|
|
||||||
title: 'MRI Receiver Coil',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.receiveCoilName,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.receiveCoilName} `,
|
|
||||||
};
|
|
||||||
const mriScanModeItem = {
|
|
||||||
id: 'MRIScanMode',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Scan Mode: ',
|
|
||||||
title: 'MRI Scan Mode',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.mrAcquisitionType,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.mrAcquisitionType} `,
|
|
||||||
};
|
|
||||||
const phaseEncodingDirectionItem = {
|
|
||||||
id: 'MRIPhaseEncodingDirection',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Phase Enc Direction: ',
|
|
||||||
title: 'MRI Phase Encoding Direction',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.phaseEncodingDirection,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.phaseEncodingDirection} `,
|
|
||||||
};
|
|
||||||
const numOfAveragesItem = {
|
|
||||||
id: 'MRINumOfAverages',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'NEX: ',
|
|
||||||
title: 'MRI Number of Averages',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.numOfAverages,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.numOfAverages} `,
|
|
||||||
};
|
|
||||||
const echoTrainLengthItem = {
|
|
||||||
id: 'MRIEchoTrainLength',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'ETL: ',
|
|
||||||
title: 'MRI Echo Train Length',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.echoTrainLength,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.echoTrainLength} `,
|
|
||||||
};
|
|
||||||
const flipAngleItem = {
|
|
||||||
id: 'MRIFlipAngle',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Flip Angle: ',
|
|
||||||
title: 'MRI Flip Angle',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.flipAngle,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.flipAngle} °`,
|
|
||||||
};
|
|
||||||
const pixelBandwidthItem = {
|
|
||||||
id: 'MRIPixelBandwidth',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Pixel Bandwidth: ',
|
|
||||||
title: 'MRI Pixel Bandwidth',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.pixelBandwidth,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.pixelBandwidth} Hz`,
|
|
||||||
};
|
|
||||||
const acquisitionTimeItem = {
|
|
||||||
id: 'MRIAcquisitionTime',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Acq Time: ',
|
|
||||||
title: 'MRI Acquisition Time',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.acquisitionTime,
|
|
||||||
contentF: ({ referenceInstance, formatters: { formatTime }, mriInstanceData }) => {
|
|
||||||
const time = mriInstanceData?.acquisitionTime
|
|
||||||
? formatTime(mriInstanceData.acquisitionTime)
|
|
||||||
: '';
|
|
||||||
return `${time}`.trim();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const acquisitionDurationTotalItem = {
|
|
||||||
id: 'MRIAcquisitionDurationTotal',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Total Acq Dur: ',
|
|
||||||
title: 'MRI Total Duration Acquisition',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.acquisitionDurationTotal,
|
|
||||||
contentF: ({ mriInstanceData }) => mriInstanceData.acquisitionDurationTotal,
|
|
||||||
};
|
|
||||||
const acquisitionDurationPerFrameItem = {
|
|
||||||
id: 'MRIAcquisitionDurationPerFrame',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Frame Acq Dur: ',
|
|
||||||
title: 'MRI Duration Acquisition Per Frame',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.acquisitionDurationPerFrame,
|
|
||||||
contentF: ({ mriInstanceData }) => mriInstanceData.acquisitionDurationPerFrame,
|
|
||||||
};
|
|
||||||
const parallelImagingItem = {
|
|
||||||
id: 'MRIParallelImaging',
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
label: 'Parallel Img: ',
|
|
||||||
title: 'MRI Parallel Imaging',
|
|
||||||
condition: ({ mriInstanceData }) => mriInstanceData?.parallelAcquisitionTechnique,
|
|
||||||
contentF: ({ mriInstanceData }) => `${mriInstanceData.parallelAcquisitionTechnique} `,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mriBottomLeftItems = {
|
|
||||||
id: 'cornerstoneOverlayBottomLeft',
|
|
||||||
items: [
|
|
||||||
scanningSequenceItem,
|
|
||||||
repetitionEchoInversionTimesItem,
|
|
||||||
receiverCoilItem,
|
|
||||||
mriScanModeItem,
|
|
||||||
phaseEncodingDirectionItem,
|
|
||||||
numOfAveragesItem,
|
|
||||||
echoTrainLengthItem,
|
|
||||||
flipAngleItem,
|
|
||||||
pixelBandwidthItem,
|
|
||||||
acquisitionDurationTotalItem,
|
|
||||||
acquisitionDurationPerFrameItem,
|
|
||||||
parallelImagingItem,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const topLeftItems = {
|
const topLeftItems = {
|
||||||
id: 'cornerstoneOverlayTopLeft',
|
id: 'cornerstoneOverlayTopLeft',
|
||||||
items: [patientNameItem, mrnItem, sexAndAgeItem],
|
items: [patientNameItem, mrnItem, sexAndAgeItem],
|
||||||
};
|
};
|
||||||
|
|
||||||
const topRightItems = {
|
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
|
||||||
id: 'cornerstoneOverlayTopRight',
|
|
||||||
items: [
|
|
||||||
patientNameItem,
|
|
||||||
mrnItem,
|
|
||||||
sexAndAgeItem,
|
|
||||||
accessionNumberItem,
|
|
||||||
studyDescriptionItem,
|
|
||||||
studyDateItem,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const bottomLeftItems = {
|
const bottomLeftItems = {
|
||||||
id: 'cornerstoneOverlayBottomLeft',
|
id: 'cornerstoneOverlayBottomLeft',
|
||||||
@@ -378,9 +160,9 @@ const bottomRightItems = {
|
|||||||
*/
|
*/
|
||||||
const CornerstoneOverlay = {
|
const CornerstoneOverlay = {
|
||||||
id: '@ohif/cornerstoneOverlay',
|
id: '@ohif/cornerstoneOverlay',
|
||||||
mriTopLeftItems,
|
topLeftItems,
|
||||||
topRightItems,
|
topRightItems,
|
||||||
mriBottomLeftItems,
|
bottomLeftItems,
|
||||||
bottomRightItems,
|
bottomRightItems,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -410,6 +192,23 @@ function CustomizableViewportOverlay({
|
|||||||
// on the individual items rather than defining individual items.
|
// on the individual items rather than defining individual items.
|
||||||
const cornerstoneOverlay = customizationService.getCustomization('@ohif/cornerstoneOverlay');
|
const cornerstoneOverlay = customizationService.getCustomization('@ohif/cornerstoneOverlay');
|
||||||
|
|
||||||
|
// Historical usage defined the overlays as separate items due to lack of
|
||||||
|
// append functionality. This code enables the historical usage, but
|
||||||
|
// the recommended functionality is to append to the default values in
|
||||||
|
// cornerstoneOverlay rather than defining individual items.
|
||||||
|
const topLeftCustomization =
|
||||||
|
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
|
||||||
|
cornerstoneOverlay?.topLeftItems;
|
||||||
|
const topRightCustomization =
|
||||||
|
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
|
||||||
|
cornerstoneOverlay?.topRightItems;
|
||||||
|
const bottomLeftCustomization =
|
||||||
|
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
|
||||||
|
cornerstoneOverlay?.bottomLeftItems;
|
||||||
|
const bottomRightCustomization =
|
||||||
|
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
|
||||||
|
cornerstoneOverlay?.bottomRightItems;
|
||||||
|
|
||||||
const instanceNumber = useMemo(
|
const instanceNumber = useMemo(
|
||||||
() =>
|
() =>
|
||||||
viewportData
|
viewportData
|
||||||
@@ -437,41 +236,8 @@ function CustomizableViewportOverlay({
|
|||||||
|
|
||||||
// FEAT: Edit overlays item [2025-09-04]
|
// FEAT: Edit overlays item [2025-09-04]
|
||||||
const studyInstanceUID = displaySetProps?.referenceInstance?.StudyInstanceUID;
|
const studyInstanceUID = displaySetProps?.referenceInstance?.StudyInstanceUID;
|
||||||
const seriesInstanceUID = displaySetProps?.referenceInstance?.SeriesInstanceUID;
|
|
||||||
const sopInstanceUID = displaySetProps?.instance?.SOPInstanceUID;
|
|
||||||
const { studyData: studyApiData, loading, error } = studyDataForOverlayItem(studyInstanceUID);
|
const { studyData: studyApiData, loading, error } = studyDataForOverlayItem(studyInstanceUID);
|
||||||
|
|
||||||
console.debug('Reference Instance:', displaySetProps?.referenceInstance);
|
|
||||||
console.debug('Instance:', displaySetProps?.instance);
|
|
||||||
|
|
||||||
// Modality untuk menentukan kustomisasi overlay
|
|
||||||
const modality = displaySetProps?.referenceInstance?.Modality;
|
|
||||||
|
|
||||||
const { instanceData: mriInstanceData } = instanceDataForMriOverlayItem(
|
|
||||||
modality === 'MR' ? studyInstanceUID : null,
|
|
||||||
modality === 'MR' ? seriesInstanceUID : null,
|
|
||||||
modality === 'MR' ? sopInstanceUID : null
|
|
||||||
);
|
|
||||||
|
|
||||||
// Historical usage defined the overlays as separate items due to lack of
|
|
||||||
// append functionality. This code enables the historical usage, but
|
|
||||||
// the recommended functionality is to append to the default values in
|
|
||||||
// cornerstoneOverlay rather than defining individual items.
|
|
||||||
const topLeftCustomization =
|
|
||||||
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
|
|
||||||
(modality === 'MR' ? cornerstoneOverlay?.mriTopLeftItems : cornerstoneOverlay?.topLeftItems);
|
|
||||||
const topRightCustomization =
|
|
||||||
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
|
|
||||||
cornerstoneOverlay?.topRightItems;
|
|
||||||
const bottomLeftCustomization =
|
|
||||||
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
|
|
||||||
(modality === 'MR'
|
|
||||||
? cornerstoneOverlay?.mriBottomLeftItems
|
|
||||||
: cornerstoneOverlay?.bottomLeftItems);
|
|
||||||
const bottomRightCustomization =
|
|
||||||
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
|
|
||||||
cornerstoneOverlay?.bottomRightItems;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updating the VOI when the viewport changes its voi
|
* Updating the VOI when the viewport changes its voi
|
||||||
*/
|
*/
|
||||||
@@ -591,7 +357,6 @@ function CustomizableViewportOverlay({
|
|||||||
viewportId,
|
viewportId,
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
studyApiData, // Pass the API data to the overlay items
|
studyApiData, // Pass the API data to the overlay items
|
||||||
mriInstanceData,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -605,7 +370,7 @@ function CustomizableViewportOverlay({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[_renderOverlayItem, displaySetProps, studyApiData, mriInstanceData]
|
[_renderOverlayItem, displaySetProps, studyApiData]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -143,130 +143,3 @@ export const studyDataForOverlayItem = (studyInstanceUID: string) => {
|
|||||||
|
|
||||||
return { studyData, loading, error };
|
return { studyData, loading, error };
|
||||||
};
|
};
|
||||||
interface MriOverlayInstanceData {
|
|
||||||
sliceThickness?: string;
|
|
||||||
sliceLocation?: string;
|
|
||||||
spacingBetweenSlices?: string;
|
|
||||||
percentPhaseFieldOfView?: string;
|
|
||||||
fieldOfViewDimensions?: string;
|
|
||||||
acquisitionMatrix?: string;
|
|
||||||
imageMatrixRows?: number;
|
|
||||||
imageMatrixColumns?: number;
|
|
||||||
scanningSequence?: string;
|
|
||||||
repetitionTime?: string;
|
|
||||||
echoTime?: string;
|
|
||||||
inversionTime?: string;
|
|
||||||
receiveCoilName?: string;
|
|
||||||
mrAcquisitionType?: string;
|
|
||||||
phaseEncodingDirection?: string;
|
|
||||||
numOfAverages?: string;
|
|
||||||
echoTrainLength?: string;
|
|
||||||
flipAngle?: string;
|
|
||||||
pixelBandwidth?: string;
|
|
||||||
acquisitionTime?: string;
|
|
||||||
acquisitionDurationTotal?: string; // in seconds
|
|
||||||
acquisitionDurationPerFrame?: string; // in ms
|
|
||||||
parallelAcquisitionTechnique?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const instanceDataForMriOverlayItem = (
|
|
||||||
studyInstanceUID: string,
|
|
||||||
seriesInstanceUID: string,
|
|
||||||
sopInstanceUID: string
|
|
||||||
) => {
|
|
||||||
const [instanceData, setInstanceData] = useState<MriOverlayInstanceData | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!studyInstanceUID || !seriesInstanceUID || !sopInstanceUID) return;
|
|
||||||
|
|
||||||
const fetchInstanceData = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const qidoRootUrl = getQidoRootUrl();
|
|
||||||
if (!qidoRootUrl) {
|
|
||||||
throw new Error('QIDO root URL not configured');
|
|
||||||
}
|
|
||||||
|
|
||||||
// MRI tags: SliceThickness(00180050), SpacingBetweenSlices(00180088),
|
|
||||||
// PercentPhaseFieldOfView(00180094), AcquisitionMatrix(00181310),
|
|
||||||
// ScanningSequence(00180020), RepetitionTime(00180080), EchoTime(00180081),
|
|
||||||
// InversionTime(00180082), ReceiveCoilName(00181250), MRAcquisitionType(00180023),
|
|
||||||
// InPlanePhaseEncodingDirection(00181312), EchoTrainLength(00180091),
|
|
||||||
// FlipAngle(00181314), PixelBandwidth(00180095), FrameAcquisitionDuration,
|
|
||||||
// ParallelAcquisitionTechnique(00189078)
|
|
||||||
// TODO: sesuaikan includeFields dengan kebutuahan jika nanti sudah bisa
|
|
||||||
// pakai includeFields di dicomweb-proxynya
|
|
||||||
const includeFields = [
|
|
||||||
'00180050',
|
|
||||||
'00180088',
|
|
||||||
'00180094',
|
|
||||||
'00181310',
|
|
||||||
'00180020',
|
|
||||||
'00180080',
|
|
||||||
'00180081',
|
|
||||||
'00180082',
|
|
||||||
'00181250',
|
|
||||||
'00180023',
|
|
||||||
'00181312',
|
|
||||||
'00180091',
|
|
||||||
'00181314',
|
|
||||||
'00180095',
|
|
||||||
'00189220',
|
|
||||||
'00189078',
|
|
||||||
].join(',');
|
|
||||||
|
|
||||||
const url = `${qidoRootUrl}/studies/${studyInstanceUID}/series/${seriesInstanceUID}/instances/${sopInstanceUID}/metadata`;
|
|
||||||
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch instance data: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
if (data && data.length > 0) {
|
|
||||||
const instance = data[0];
|
|
||||||
|
|
||||||
const extractedData: MriOverlayInstanceData = {
|
|
||||||
sliceThickness: instance['00180050']?.Value?.[0],
|
|
||||||
sliceLocation: instance['00201041']?.Value?.[0],
|
|
||||||
spacingBetweenSlices: instance['00180088']?.Value?.[0],
|
|
||||||
percentPhaseFieldOfView: instance['00180094']?.Value?.[0],
|
|
||||||
fieldOfViewDimensions: instance['00181149']?.Value,
|
|
||||||
acquisitionMatrix: instance['00181310']?.Value,
|
|
||||||
imageMatrixRows: instance['00280010']?.Value?.[0],
|
|
||||||
imageMatrixColumns: instance['00280011']?.Value?.[0],
|
|
||||||
scanningSequence: instance['00180020']?.Value?.[0],
|
|
||||||
repetitionTime: instance['00180080']?.Value?.[0],
|
|
||||||
echoTime: instance['00180081']?.Value?.[0],
|
|
||||||
inversionTime: instance['00180082']?.Value?.[0],
|
|
||||||
receiveCoilName: instance['00181250']?.Value?.[0],
|
|
||||||
mrAcquisitionType: instance['00180023']?.Value?.[0],
|
|
||||||
phaseEncodingDirection: instance['00181312']?.Value?.[0],
|
|
||||||
numOfAverages: instance['00180083']?.Value?.[0],
|
|
||||||
echoTrainLength: instance['00180091']?.Value?.[0],
|
|
||||||
flipAngle: instance['00181314']?.Value?.[0],
|
|
||||||
pixelBandwidth: instance['00180095']?.Value?.[0],
|
|
||||||
acquisitionTime: instance['00080032']?.Value?.[0],
|
|
||||||
acquisitionDurationTotal: instance['00189073']?.Value?.[0],
|
|
||||||
acquisitionDurationPerFrame: instance['00189220']?.Value?.[0],
|
|
||||||
parallelAcquisitionTechnique: instance['00189078']?.Value?.[0],
|
|
||||||
};
|
|
||||||
|
|
||||||
console.debug('Extracted MRI Instance Data:', extractedData);
|
|
||||||
setInstanceData(extractedData);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching MRI instance data:', err);
|
|
||||||
setError(err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchInstanceData();
|
|
||||||
}, [studyInstanceUID, seriesInstanceUID, sopInstanceUID]);
|
|
||||||
|
|
||||||
return { instanceData, loading, error };
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,407 +0,0 @@
|
|||||||
/** @type {AppTypes.Config} */
|
|
||||||
|
|
||||||
window.config = {
|
|
||||||
routerBasename: '/',
|
|
||||||
// whiteLabeling: {},
|
|
||||||
extensions: [],
|
|
||||||
modes: [],
|
|
||||||
customizationService: {},
|
|
||||||
showStudyList: true,
|
|
||||||
// some windows systems have issues with more than 3 web workers
|
|
||||||
maxNumberOfWebWorkers: 3,
|
|
||||||
// below flag is for performance reasons, but it might not work for all servers
|
|
||||||
showWarningMessageForCrossOrigin: true,
|
|
||||||
showCPUFallbackMessage: true,
|
|
||||||
showLoadingIndicator: true,
|
|
||||||
experimentalStudyBrowserSort: false,
|
|
||||||
strictZSpacingForVolumeViewport: true,
|
|
||||||
groupEnabledModesFirst: true,
|
|
||||||
maxNumRequests: {
|
|
||||||
interaction: 100,
|
|
||||||
thumbnail: 75,
|
|
||||||
// Prefetch number is dependent on the http protocol. For http 2 or
|
|
||||||
// above, the number of requests can be go a lot higher.
|
|
||||||
prefetch: 25,
|
|
||||||
},
|
|
||||||
expertise_host: `http://192.168.0.137:8080/api/v1/radiology/expertise/image-number`, // API his3 untuk expertise radiologi
|
|
||||||
pacs_document_host: `152.42.173.210`, // IP ke NV di PACS Server untuk ambil pdf
|
|
||||||
pacs_document_port: 8585,
|
|
||||||
defaultDataSourceName: 'local-proxy',
|
|
||||||
dataSources: [
|
|
||||||
{
|
|
||||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
|
||||||
sourceName: 'local-proxy',
|
|
||||||
configuration: {
|
|
||||||
friendlyName: 'Static WADO Local Data',
|
|
||||||
name: 'DCM4CHEE',
|
|
||||||
qidoRoot: `http://152.42.173.210:5000/rs`, // IP ke dicomweb-proxy PACS Server. URI selalu /rs
|
|
||||||
wadoRoot: `http://152.42.173.210:5000/rs`, // IP ke dicomweb-proxy PACS Server. URI selalu /rs
|
|
||||||
qidoSupportsIncludeField: false,
|
|
||||||
supportsReject: true,
|
|
||||||
supportsStow: true,
|
|
||||||
imageRendering: 'wadors',
|
|
||||||
thumbnailRendering: 'wadors',
|
|
||||||
enableStudyLazyLoad: true,
|
|
||||||
supportsFuzzyMatching: false,
|
|
||||||
supportsWildcard: true,
|
|
||||||
staticWado: true,
|
|
||||||
singlepart: 'video',
|
|
||||||
bulkDataURI: {
|
|
||||||
enabled: true,
|
|
||||||
relativeResolution: 'studies',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy',
|
|
||||||
sourceName: 'dicomwebproxy',
|
|
||||||
configuration: {
|
|
||||||
friendlyName: 'dicomweb delegating proxy',
|
|
||||||
name: 'dicomwebproxy',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomjson',
|
|
||||||
sourceName: 'dicomjson',
|
|
||||||
configuration: {
|
|
||||||
friendlyName: 'dicom json',
|
|
||||||
name: 'json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal',
|
|
||||||
sourceName: 'dicomlocal',
|
|
||||||
configuration: {
|
|
||||||
friendlyName: 'dicom local',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
httpErrorHandler: error => {
|
|
||||||
// This is 429 when rejected from the public idc sandbox too often.
|
|
||||||
console.warn(error.status);
|
|
||||||
|
|
||||||
// Could use services manager here to bring up a dialog/modal if needed.
|
|
||||||
console.warn('test, navigate to https://ohif.org/');
|
|
||||||
},
|
|
||||||
// whiteLabeling: {
|
|
||||||
// /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
|
|
||||||
// createLogoComponentFn: function (React) {
|
|
||||||
// return React.createElement(
|
|
||||||
// 'a',
|
|
||||||
// {
|
|
||||||
// target: '_self',
|
|
||||||
// rel: 'noopener noreferrer',
|
|
||||||
// className: 'text-purple-600 line-through',
|
|
||||||
// href: '/',
|
|
||||||
// },
|
|
||||||
// React.createElement('img',
|
|
||||||
// {
|
|
||||||
// src: './assets/customLogo.svg',
|
|
||||||
// className: 'w-8 h-8',
|
|
||||||
// }
|
|
||||||
// ))
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
hotkeys: [
|
|
||||||
{
|
|
||||||
commandName: 'incrementActiveViewport',
|
|
||||||
label: 'Next Viewport',
|
|
||||||
keys: ['right'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'decrementActiveViewport',
|
|
||||||
label: 'Previous Viewport',
|
|
||||||
keys: ['left'],
|
|
||||||
},
|
|
||||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
|
||||||
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
|
|
||||||
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
|
|
||||||
{
|
|
||||||
commandName: 'flipViewportHorizontal',
|
|
||||||
label: 'Flip Horizontally',
|
|
||||||
keys: ['h'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'flipViewportVertical',
|
|
||||||
label: 'Flip Vertically',
|
|
||||||
keys: ['v'],
|
|
||||||
},
|
|
||||||
{ commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] },
|
|
||||||
{ commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] },
|
|
||||||
{ commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] },
|
|
||||||
{ commandName: 'resetViewport', label: 'Reset', keys: ['space'] },
|
|
||||||
{ commandName: 'nextImage', label: 'Next Image', keys: ['down'] },
|
|
||||||
{ commandName: 'previousImage', label: 'Previous Image', keys: ['up'] },
|
|
||||||
// {
|
|
||||||
// commandName: 'previousViewportDisplaySet',
|
|
||||||
// label: 'Previous Series',
|
|
||||||
// keys: ['pagedown'],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// commandName: 'nextViewportDisplaySet',
|
|
||||||
// label: 'Next Series',
|
|
||||||
// keys: ['pageup'],
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Zoom' },
|
|
||||||
label: 'Zoom',
|
|
||||||
keys: ['z'],
|
|
||||||
},
|
|
||||||
// ~ Window level presets
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset1',
|
|
||||||
label: 'W/L Preset 1',
|
|
||||||
keys: ['1'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset2',
|
|
||||||
label: 'W/L Preset 2',
|
|
||||||
keys: ['2'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset3',
|
|
||||||
label: 'W/L Preset 3',
|
|
||||||
keys: ['3'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset4',
|
|
||||||
label: 'W/L Preset 4',
|
|
||||||
keys: ['4'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset5',
|
|
||||||
label: 'W/L Preset 5',
|
|
||||||
keys: ['5'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset6',
|
|
||||||
label: 'W/L Preset 6',
|
|
||||||
keys: ['6'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset7',
|
|
||||||
label: 'W/L Preset 7',
|
|
||||||
keys: ['7'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset8',
|
|
||||||
label: 'W/L Preset 8',
|
|
||||||
keys: ['8'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset9',
|
|
||||||
label: 'W/L Preset 9',
|
|
||||||
keys: ['9'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
tours: [
|
|
||||||
{
|
|
||||||
id: 'basicViewerTour',
|
|
||||||
route: '/viewer',
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
id: 'scroll',
|
|
||||||
title: 'Scrolling Through Images',
|
|
||||||
text: 'You can scroll through the images using the mouse wheel or scrollbar.',
|
|
||||||
attachTo: {
|
|
||||||
element: '.viewport-element',
|
|
||||||
on: 'top',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: '.cornerstone-viewport-element',
|
|
||||||
event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () => waitForElement('.viewport-element'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'zoom',
|
|
||||||
title: 'Zooming In and Out',
|
|
||||||
text: 'You can zoom the images using the right click.',
|
|
||||||
attachTo: {
|
|
||||||
element: '.viewport-element',
|
|
||||||
on: 'left',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: '.cornerstone-viewport-element',
|
|
||||||
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () => waitForElement('.viewport-element'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'pan',
|
|
||||||
title: 'Panning the Image',
|
|
||||||
text: 'You can pan the images using the middle click.',
|
|
||||||
attachTo: {
|
|
||||||
element: '.viewport-element',
|
|
||||||
on: 'top',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: '.cornerstone-viewport-element',
|
|
||||||
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () => waitForElement('.viewport-element'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'windowing',
|
|
||||||
title: 'Adjusting Window Level',
|
|
||||||
text: 'You can modify the window level using the left click.',
|
|
||||||
attachTo: {
|
|
||||||
element: '.viewport-element',
|
|
||||||
on: 'left',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: '.cornerstone-viewport-element',
|
|
||||||
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () => waitForElement('.viewport-element'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'length',
|
|
||||||
title: 'Using the Measurement Tools',
|
|
||||||
text: 'You can measure the length of a region using the Length tool.',
|
|
||||||
attachTo: {
|
|
||||||
element: '[data-cy="MeasurementTools-split-button-primary"]',
|
|
||||||
on: 'bottom',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: '[data-cy="MeasurementTools-split-button-primary"]',
|
|
||||||
event: 'click',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () =>
|
|
||||||
waitForElement('[data-cy="MeasurementTools-split-button-primary]'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'drawAnnotation',
|
|
||||||
title: 'Drawing Length Annotations',
|
|
||||||
text: 'Use the length tool on the viewport to measure the length of a region.',
|
|
||||||
attachTo: {
|
|
||||||
element: '.viewport-element',
|
|
||||||
on: 'right',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: 'body',
|
|
||||||
event: 'event::measurement_added',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () => waitForElement('.viewport-element'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'trackMeasurement',
|
|
||||||
title: 'Tracking Measurements in the Panel',
|
|
||||||
text: 'Click yes to track the measurements in the measurement panel.',
|
|
||||||
attachTo: {
|
|
||||||
element: '[data-cy="prompt-begin-tracking-yes-btn"]',
|
|
||||||
on: 'bottom',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: '[data-cy="prompt-begin-tracking-yes-btn"]',
|
|
||||||
event: 'click',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () => waitForElement('[data-cy="prompt-begin-tracking-yes-btn"]'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'openMeasurementPanel',
|
|
||||||
title: 'Opening the Measurements Panel',
|
|
||||||
text: 'Click the measurements button to open the measurements panel.',
|
|
||||||
attachTo: {
|
|
||||||
element: '#trackedMeasurements-btn',
|
|
||||||
on: 'left-start',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: '#trackedMeasurements-btn',
|
|
||||||
event: 'click',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () => waitForElement('#trackedMeasurements-btn'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'scrollAwayFromMeasurement',
|
|
||||||
title: 'Scrolling Away from a Measurement',
|
|
||||||
text: 'Scroll the images using the mouse wheel away from the measurement.',
|
|
||||||
attachTo: {
|
|
||||||
element: '.viewport-element',
|
|
||||||
on: 'left',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: '.cornerstone-viewport-element',
|
|
||||||
event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () => waitForElement('.viewport-element'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'jumpToMeasurement',
|
|
||||||
title: 'Jumping to Measurements in the Panel',
|
|
||||||
text: 'Click the measurement in the measurement panel to jump to it.',
|
|
||||||
attachTo: {
|
|
||||||
element: '[data-cy="data-row"]',
|
|
||||||
on: 'left-start',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: '[data-cy="data-row"]',
|
|
||||||
event: 'click',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () => waitForElement('[data-cy="data-row"]'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'changeLayout',
|
|
||||||
title: 'Changing Layout',
|
|
||||||
text: 'You can change the layout of the viewer using the layout button.',
|
|
||||||
attachTo: {
|
|
||||||
element: '[data-cy="Layout"]',
|
|
||||||
on: 'bottom',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: '[data-cy="Layout"]',
|
|
||||||
event: 'click',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () => waitForElement('[data-cy="Layout"]'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'selectLayout',
|
|
||||||
title: 'Selecting the MPR Layout',
|
|
||||||
text: 'Select the MPR layout to view the images in MPR mode.',
|
|
||||||
attachTo: {
|
|
||||||
element: '[data-cy="MPR"]',
|
|
||||||
on: 'left-start',
|
|
||||||
},
|
|
||||||
advanceOn: {
|
|
||||||
selector: '[data-cy="MPR"]',
|
|
||||||
event: 'click',
|
|
||||||
},
|
|
||||||
beforeShowPromise: () => waitForElement('[data-cy="MPR"]'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
tourOptions: {
|
|
||||||
useModalOverlay: true,
|
|
||||||
defaultStepOptions: {
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: 'Skip all',
|
|
||||||
action() {
|
|
||||||
this.complete();
|
|
||||||
},
|
|
||||||
secondary: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
function waitForElement(selector, maxAttempts = 20, interval = 25) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
let attempts = 0;
|
|
||||||
|
|
||||||
const checkForElement = setInterval(() => {
|
|
||||||
const element = document.querySelector(selector);
|
|
||||||
|
|
||||||
if (element || attempts >= maxAttempts) {
|
|
||||||
clearInterval(checkForElement);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts++;
|
|
||||||
}, interval);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -341,8 +341,7 @@ const SidePanel = ({
|
|||||||
fetchAccessionNumber();
|
fetchAccessionNumber();
|
||||||
}, [studyInstanceUID]); // Run when studyInstanceUID changes
|
}, [studyInstanceUID]); // Run when studyInstanceUID changes
|
||||||
|
|
||||||
const [expertiseError, setExpertiseError] = useState(null);
|
// Ubah fungsi fetchExpertiseData menjadi dengan parameter accessionNumber
|
||||||
|
|
||||||
const fetchExpertiseData = async accessionNumber => {
|
const fetchExpertiseData = async accessionNumber => {
|
||||||
try {
|
try {
|
||||||
// Check if window.config.expertise_host exists
|
// Check if window.config.expertise_host exists
|
||||||
@@ -357,42 +356,45 @@ const SidePanel = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsExpertiseLoading(true);
|
setIsExpertiseLoading(true);
|
||||||
const url = `${window.config.expertise_host}/${encodeURIComponent(accessionNumber)}`;
|
const url = `${window.config.expertise_host}/nv/query.php?method=view&AccessionNumber=${encodeURIComponent(accessionNumber)}`;
|
||||||
|
|
||||||
|
// Debuggging
|
||||||
|
// const url = 'http://152.42.173.210/nv/testQueryBase64.php';
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const resp = await response.json();
|
const data = await response.json();
|
||||||
console.log('Expertise Resp:', resp);
|
console.log('Study data:', data);
|
||||||
|
|
||||||
if (resp?.error) {
|
if (data?.study?.expertise && data.study.expertise.length > 0) {
|
||||||
setExpertiseError(resp.message || resp.error);
|
const expertiseItem = { ...data.study.expertise[0] };
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resp?.status_code === 200 && resp?.data) {
|
|
||||||
const expertiseItem = { ...resp.data };
|
|
||||||
|
|
||||||
// Decode base64 encoded fields
|
// Decode base64 encoded fields
|
||||||
|
// UTF-8 safe base64 decoding
|
||||||
const decodeBase64 = str => {
|
const decodeBase64 = str => {
|
||||||
try {
|
try {
|
||||||
|
// Step 1: decode base64 to binary
|
||||||
const binary = atob(str);
|
const binary = atob(str);
|
||||||
|
// Step 2: create a Uint8Array from the binary string
|
||||||
const bytes = new Uint8Array(binary.length);
|
const bytes = new Uint8Array(binary.length);
|
||||||
for (let i = 0; i < binary.length; i++) {
|
for (let i = 0; i < binary.length; i++) {
|
||||||
bytes[i] = binary.charCodeAt(i);
|
bytes[i] = binary.charCodeAt(i);
|
||||||
}
|
}
|
||||||
|
// Step 3: decode the Uint8Array as UTF-8
|
||||||
return new TextDecoder('utf-8').decode(bytes);
|
return new TextDecoder('utf-8').decode(bytes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error decoding base64 string:', e);
|
console.error('Error decoding base64 string:', e);
|
||||||
return str;
|
return str; // Return original if decoding fails
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Decode known base64 fields
|
// Decode known base64 fields
|
||||||
if (expertiseItem.ekspertiseResult) {
|
if (expertiseItem.expertise) {
|
||||||
expertiseItem.ekspertiseResult = decodeBase64(expertiseItem.ekspertiseResult);
|
expertiseItem.expertise = decodeBase64(expertiseItem.expertise);
|
||||||
}
|
}
|
||||||
if (expertiseItem.radiologist) {
|
if (expertiseItem.radiologist) {
|
||||||
expertiseItem.radiologist = decodeBase64(expertiseItem.radiologist);
|
expertiseItem.radiologist = decodeBase64(expertiseItem.radiologist);
|
||||||
}
|
}
|
||||||
|
// Add any other fields that might be base64 encoded
|
||||||
|
|
||||||
setExpertiseData(expertiseItem);
|
setExpertiseData(expertiseItem);
|
||||||
}
|
}
|
||||||
@@ -414,45 +416,83 @@ const SidePanel = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expertiseError) {
|
|
||||||
return (
|
|
||||||
<ScrollArea className="border-input bg-background h-[500px] w-full rounded-md border py-2 px-3 text-base text-white">
|
|
||||||
<h3 className="mb-4 text-lg font-bold">Expertise</h3>
|
|
||||||
<p className="text-red-500">{expertiseError}</p>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!expertiseData) {
|
if (!expertiseData) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseExpertise = text => {
|
||||||
|
if (!text) return {};
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
let currentSection = 'Keterangan';
|
||||||
|
|
||||||
|
// Split expertise text by lines and process each line
|
||||||
|
const lines = text.split('\r\n').filter(line => line.trim() !== '');
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
// Check if this is a section header
|
||||||
|
if (line.includes(':') && !line.trim().startsWith('-')) {
|
||||||
|
const parts = line.split(':');
|
||||||
|
currentSection = parts[0].trim();
|
||||||
|
const value = parts[1]?.trim() || '';
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
if (!result[currentSection]) {
|
||||||
|
result[currentSection] = [];
|
||||||
|
}
|
||||||
|
result[currentSection].push(value);
|
||||||
|
}
|
||||||
|
} else if (line.toLowerCase().includes('kesan')) {
|
||||||
|
currentSection = 'Kesan';
|
||||||
|
} else {
|
||||||
|
// Add line to current section
|
||||||
|
if (!result[currentSection]) {
|
||||||
|
result[currentSection] = [];
|
||||||
|
}
|
||||||
|
result[currentSection].push(line.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsedSections = parseExpertise(expertiseData.expertise);
|
||||||
|
|
||||||
// Create formatted data structure
|
// Create formatted data structure
|
||||||
const formattedData = [
|
const formattedData = [
|
||||||
{ label: 'Dokter Pengirim', value: expertiseData.orderingPhysician || '' },
|
{ label: 'Dokter Pengirim', value: expertiseData.ordering_physician || '' },
|
||||||
{ label: 'Dokter Radiologis', value: expertiseData.radiologist || '' },
|
{ label: 'Dokter Radiologis', value: expertiseData.radiologist || '' },
|
||||||
{ label: 'Waktu Expertise', value: expertiseData.expertiseDttm || '' },
|
{ label: 'Waktu Expertise', value: expertiseData.expertise_dttm || '' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add additional sections from parsed text
|
||||||
|
Object.entries(parsedSections).forEach(([key, value]) => {
|
||||||
|
formattedData.push({
|
||||||
|
label: key,
|
||||||
|
value: Array.isArray(value) ? value : [value],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="border-input bg-background h-[800px] w-full rounded-md border py-2 px-3 text-base text-white">
|
<ScrollArea className="border-input bg-background h-[500px] w-full rounded-md border p-2 text-sm text-white">
|
||||||
<h3 className="mb-4 text-xl font-bold">Expertise</h3>
|
<h3 className="mb-4 text-lg font-bold">Expertise</h3>
|
||||||
{formattedData.map((section, index) => (
|
{formattedData.map((section, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
>
|
>
|
||||||
<h5 className="text-lg font-bold">{section.label}:</h5>
|
<h5 className="text-base font-bold">{section.label}:</h5>
|
||||||
<p className="break-words">{section.value}</p>
|
{Array.isArray(section.value) ? (
|
||||||
|
<ul className="list-disc pl-6">
|
||||||
|
{section.value.map((item, idx) => (
|
||||||
|
<li key={idx}>{item}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="break-words">{section.value}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="mb-4">
|
|
||||||
<h5 className="text-lg font-bold">Hasil Expertise:</h5>
|
|
||||||
<div
|
|
||||||
className="leading-6"
|
|
||||||
dangerouslySetInnerHTML={{ __html: expertiseData.ekspertiseResult || '' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user