diff --git a/.gitignore b/.gitignore index af800d7..57c0587 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ tests/test-results/ tests/playwright-report/ /blob-report/ /playwright/.cache/ + +# Dummy +/dump diff --git a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx index 0883b06..29108e5 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx @@ -9,6 +9,7 @@ import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } fro import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import './CustomizableViewportOverlay.css'; +import { studyDataForOverlayItem } from './studyDataForOverlayItem'; const EPSILON = 1e-4; @@ -51,20 +52,31 @@ const OverlayItemComponents = { 'ohif.overlayItem.expertise': ExpertiseOverlayItem, }; +/* + // FEAT: Edit overlays item [2025-09-04] + // Merubah tampilan overlay item menjadi mirip NV (OHIF v1) + // Sebagian data dipanggil melalui fetching API di ./studayDataForOverlayItem.ts + // karena tidak ada di referenceInstance +*/ + const studyDateItem = { id: 'StudyDate', customizationType: 'ohif.overlayItem', - label: '', - title: 'Study date', - condition: ({ referenceInstance }) => referenceInstance?.StudyDate, - contentF: ({ referenceInstance, formatters: { formatDate } }) => - formatDate(referenceInstance.StudyDate), + label: 'Study Dttm: ', + title: 'Study date and time', + condition: ({ referenceInstance, studyApiData }) => + referenceInstance?.StudyDate || studyApiData?.studyTime, + contentF: ({ referenceInstance, formatters: { formatDate }, studyApiData }) => { + const date = referenceInstance?.StudyDate ? formatDate(referenceInstance.StudyDate) : ''; + const time = studyApiData?.studyTime ? studyApiData.studyTime : ''; + return `${date} ${time}`; + }, }; const seriesDescriptionItem = { id: 'SeriesDescription', customizationType: 'ohif.overlayItem', - label: '', + label: 'Series Desc: ', title: 'Series description', condition: ({ referenceInstance }) => { return referenceInstance && referenceInstance.SeriesDescription; @@ -72,33 +84,86 @@ const seriesDescriptionItem = { contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription, }; +const patientNameItem = { + id: 'PatientName', + customizationType: 'ohif.overlayItem', + label: '', + title: 'Patient Name', + condition: ({ instance }) => instance?.PatientName, + contentF: ({ instance, formatters: { formatPN } }) => formatPN(instance.PatientName.Alphabetic), +}; + +const mrnItem = { + id: 'MRN', + customizationType: 'ohif.overlayItem', + label: '', + title: 'Medical Record Number', + condition: ({ referenceInstance }) => referenceInstance?.PatientID, + contentF: ({ referenceInstance }) => referenceInstance.PatientID, +}; + +const sexAndAgeItem = { + id: 'SexAndAge', + customizationType: 'ohif.overlayItem', + label: '', + title: 'Patient Sex and Age', + condition: ({ studyApiData }) => studyApiData?.patientSex || studyApiData?.formattedAge, + contentF: ({ studyApiData }) => { + const sex = studyApiData?.patientSex || ''; + const age = studyApiData?.formattedAge || ''; + return sex && age ? `${sex}, ${age}` : sex || age; + }, +}; + +const studyDescriptionItem = { + id: 'StudyDescription', + customizationType: 'ohif.overlayItem', + label: 'Study Desc: ', + title: 'Study Description', + condition: ({ studyApiData }) => studyApiData?.studyDescription, + contentF: ({ studyApiData }) => studyApiData.studyDescription, +}; + +const accessionNumberItem = { + id: 'AccessionNumber', + customizationType: 'ohif.overlayItem', + label: '', + title: 'Accession Number', + condition: ({ studyApiData }) => studyApiData?.accessionNumber, + contentF: ({ studyApiData }) => studyApiData.accessionNumber, +}; + const topLeftItems = { id: 'cornerstoneOverlayTopLeft', - items: [studyDateItem, seriesDescriptionItem], + items: [patientNameItem, mrnItem, sexAndAgeItem], }; const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] }; +// const bottomLeftItems = { +// id: 'cornerstoneOverlayBottomLeft', +// items: [ +// { +// id: 'Expertise', +// customizationType: 'ohif.overlayItem.expertise', +// }, +// { +// id: 'WindowLevel', +// customizationType: 'ohif.overlayItem.windowLevel', +// }, +// { +// id: 'ZoomLevel', +// customizationType: 'ohif.overlayItem.zoomLevel', +// condition: props => { +// const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId); +// return activeToolName === 'Zoom'; +// }, +// }, +// ], +// }; const bottomLeftItems = { id: 'cornerstoneOverlayBottomLeft', - items: [ - { - id: 'Expertise', - customizationType: 'ohif.overlayItem.expertise', - }, - { - id: 'WindowLevel', - customizationType: 'ohif.overlayItem.windowLevel', - }, - { - id: 'ZoomLevel', - customizationType: 'ohif.overlayItem.zoomLevel', - condition: props => { - const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId); - return activeToolName === 'Zoom'; - }, - }, - ], + items: [accessionNumberItem, studyDescriptionItem, studyDateItem, seriesDescriptionItem], }; const bottomRightItems = { @@ -108,6 +173,7 @@ const bottomRightItems = { id: 'InstanceNumber', customizationType: 'ohif.overlayItem.instanceNumber', }, + seriesDescriptionItem, ], }; @@ -197,6 +263,10 @@ function CustomizableViewportOverlay({ }; }, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]); + // FEAT: Edit overlays item [2025-09-04] + const studyInstanceUID = displaySetProps?.referenceInstance?.StudyInstanceUID; + const { studyData: studyApiData, loading, error } = studyDataForOverlayItem(studyInstanceUID); + /** * Updating the VOI when the viewport changes its voi */ @@ -315,6 +385,7 @@ function CustomizableViewportOverlay({ instanceNumber, viewportId, toolGroupService, + studyApiData, // Pass the API data to the overlay items }; return ( @@ -328,7 +399,7 @@ function CustomizableViewportOverlay({ ); }, - [_renderOverlayItem, displaySetProps] + [_renderOverlayItem, displaySetProps, studyApiData] ); return ( @@ -539,6 +610,8 @@ function ExpertiseOverlayItem({ displaySetProps, customization }: OverlayItemPro const [expertise, setExpertise] = useState(null); const { referenceInstance } = displaySetProps || {}; + console.log(displaySetProps); + useEffect(() => { if (!referenceInstance?.StudyInstanceUID) return; @@ -549,8 +622,7 @@ function ExpertiseOverlayItem({ displaySetProps, customization }: OverlayItemPro ); const data = await response.json(); setExpertise(data.result); - } catch (error) { - } + } catch (error) {} }; // Get the expertise config value @@ -565,11 +637,36 @@ function ExpertiseOverlayItem({ displaySetProps, customization }: OverlayItemPro if (!expertise) return null; return ( -
-
Expertise:
+
+
+ Expertise: +
{expertise}
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestiae facilis, illum ex repellendus nam dicta saepe fuga officia distinctio cupiditate vitae, nihil neque tenetur possimus nulla suscipit, ab laboriosam. Nisi sit debitis amet distinctio, inventore sint beatae a eius excepturi aperiam recusandae odit magnam qui quos accusamus nemo eaque iusto, error ex provident repellat! Tempora, quos quisquam. Eveniet unde sed aliquam iste, dignissimos est soluta incidunt, magnam voluptatem totam illo maiores. Nostrum repellat itaque ipsa quae tempora in corrupti labore, nihil quisquam error sapiente temporibus minima inventore delectus dolores doloribus laboriosam saepe laudantium cumque accusamus id aut unde eum! Et quasi natus, veniam neque dolorem, aut facere eos magni animi est aspernatur eligendi? Cumque repellat dolorem, autem ex quis, voluptatem eos minima iure nulla laudantium ea asperiores possimus assumenda recusandae nobis, aperiam exercitationem totam molestiae aut obcaecati illo. Nihil dolorum qui rem fugiat optio consequuntur inventore sequi, eveniet provident, ipsam incidunt repudiandae porro temporibus omnis deserunt mollitia nisi at pariatur. Ea fuga quisquam mollitia architecto, temporibus numquam asperiores reiciendis nam, dolores doloremque accusamus nostrum, illum possimus sint dolorem soluta tenetur tempore laborum natus excepturi assumenda non atque vitae enim! Adipisci illo corrupti quibusdam sapiente omnis quaerat voluptatum deleniti fuga deserunt.
-


+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestiae facilis, illum ex + repellendus nam dicta saepe fuga officia distinctio cupiditate vitae, nihil neque tenetur + possimus nulla suscipit, ab laboriosam. Nisi sit debitis amet distinctio, inventore sint + beatae a eius excepturi aperiam recusandae odit magnam qui quos accusamus nemo eaque iusto, + error ex provident repellat! Tempora, quos quisquam. Eveniet unde sed aliquam iste, + dignissimos est soluta incidunt, magnam voluptatem totam illo maiores. Nostrum repellat + itaque ipsa quae tempora in corrupti labore, nihil quisquam error sapiente temporibus minima + inventore delectus dolores doloribus laboriosam saepe laudantium cumque accusamus id aut + unde eum! Et quasi natus, veniam neque dolorem, aut facere eos magni animi est aspernatur + eligendi? Cumque repellat dolorem, autem ex quis, voluptatem eos minima iure nulla + laudantium ea asperiores possimus assumenda recusandae nobis, aperiam exercitationem totam + molestiae aut obcaecati illo. Nihil dolorum qui rem fugiat optio consequuntur inventore + sequi, eveniet provident, ipsam incidunt repudiandae porro temporibus omnis deserunt + mollitia nisi at pariatur. Ea fuga quisquam mollitia architecto, temporibus numquam + asperiores reiciendis nam, dolores doloremque accusamus nostrum, illum possimus sint dolorem + soluta tenetur tempore laborum natus excepturi assumenda non atque vitae enim! Adipisci illo + corrupti quibusdam sapiente omnis quaerat voluptatum deleniti fuga deserunt. +
+
+

+
); } diff --git a/extensions/cornerstone/src/Viewport/Overlays/studyDataForOverlayItem.ts b/extensions/cornerstone/src/Viewport/Overlays/studyDataForOverlayItem.ts new file mode 100644 index 0000000..71f3b87 --- /dev/null +++ b/extensions/cornerstone/src/Viewport/Overlays/studyDataForOverlayItem.ts @@ -0,0 +1,145 @@ +/** + * Hook kustom untuk mengambil dan mengelola data studi berdasarkan StudyInstanceUID. + * + * Hook ini mengambil informasi terkait studi dari endpoint layanan QIDO + * dan memprosesnya untuk digunakan dalam overlay atau komponen UI lainnya. + * Ini menangani status pemuatan, kesalahan, dan ekstraksi data dari field metadata DICOM. + * + * @param studyInstanceUID - Identifier unik untuk studi yang datanya akan diambil. + * @returns Sebuah objek yang berisi: + * - `studyData`: Data studi yang telah diekstrak (atau `null` jika belum dimuat). + * - `loading`: Boolean yang menunjukkan apakah data sedang diambil. + * - `error`: Objek error jika proses pengambilan data gagal, atau `null` jika berhasil. + * + * @catatan + * Hook ini bergantung pada fungsi `getQidoRootUrl` untuk menentukan endpoint layanan QIDO. + * Pastikan fungsi ini dikonfigurasi dengan benar di aplikasi Anda. + * + * Field DICOM berikut akan diekstrak dan diproses: + * - Study Description (0008,1030) + * - Accession Number (0008,0050) + * - Study Time (0008,0030) + * - Patient Age (0010,1010) + * - Patient Sex (0010,0040) + * - Patient Birth Date (0010,0030) + * + * Jika tanggal lahir pasien tersedia, umur akan dihitung dan diformat. + */ + +import { useState, useEffect } from 'react'; + +interface StudyData { + studyDescription?: string; + accessionNumber?: string; + studyTime?: string; + patientAge?: string; + patientSex?: string; + patientBirthDate?: string; + formattedAge?: string; +} + +const calculateAge = (birthDateString: string): string => { + if (!birthDateString) return ''; + + // DICOM date format is YYYYMMDD + const year = parseInt(birthDateString.substring(0, 4)); + const month = parseInt(birthDateString.substring(4, 6)) - 1; // JS months are 0-indexed + const day = parseInt(birthDateString.substring(6, 8)); + + const birthDate = new Date(year, month, day); + const today = new Date(); + + let ageYears = today.getFullYear() - birthDate.getFullYear(); + let ageMonths = today.getMonth() - birthDate.getMonth(); + let ageDays = today.getDate() - birthDate.getDate(); + + // Adjust for negative months or days + if (ageDays < 0) { + ageMonths--; + // Get days in the previous month + const prevMonthDate = new Date(today.getFullYear(), today.getMonth(), 0); + ageDays += prevMonthDate.getDate(); + } + + if (ageMonths < 0) { + ageYears--; + ageMonths += 12; + } + + return `${ageYears}Y ${ageMonths}M ${ageDays}D`; +}; + +const getQidoRootUrl = (): string => { + const { config } = window as any; + + if (!config || !config.dataSources || !config.defaultDataSourceName) { + console.warn('Configuration not found, using fallback URL'); + return; + } + + const dataSource = config.dataSources.find( + (ds: any) => ds.sourceName === config.defaultDataSourceName + ); + + if (!dataSource || !dataSource.configuration || !dataSource.configuration.qidoRoot) { + console.warn('Data source configuration not found, return nothing'); + return; + } + + return dataSource.configuration.qidoRoot; +}; + +export const studyDataForOverlayItem = (studyInstanceUID: string) => { + const [studyData, setStudyData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!studyInstanceUID) return; + + const fetchStudyData = async () => { + setLoading(true); + try { + const qidoRootUrl = getQidoRootUrl(); + + const response = await fetch( + `${qidoRootUrl}/studies?limit=101&offset=0&fuzzymatching=false&includefield=00080050,00081030,00101010,0010004&StudyInstanceUID=${studyInstanceUID}` + ); + + if (!response.ok) { + throw new Error('Failed to fetch study data'); + } + + const data = await response.json(); + + if (data && data.length > 0) { + const studyInfo = data[0]; + + const birthDate = studyInfo['00100030']?.Value?.[0] || ''; + const formattedAge = calculateAge(birthDate); + + const extractedData: StudyData = { + studyDescription: studyInfo['00081030']?.Value?.[0] || '', + accessionNumber: studyInfo['00080050']?.Value?.[0] || '', + studyTime: studyInfo['00080030']?.Value?.[0] || '', + patientAge: studyInfo['00101010']?.Value?.[0] || '', + patientSex: studyInfo['00100040']?.Value?.[0] || '', + patientBirthDate: birthDate, + formattedAge: formattedAge, + }; + console.log('Extracted Study Data:', extractedData); + setStudyData(extractedData); + } + } catch (err) { + console.error('Error fetching study data:', err); + setError(err); + } finally { + setLoading(false); + } + }; + + fetchStudyData(); + }, [studyInstanceUID]); + + return { studyData, loading, error }; +};