diff --git a/platform/app/public/config/default_phkg.js b/platform/app/public/config/default_phkg.js new file mode 100644 index 0000000..82fde61 --- /dev/null +++ b/platform/app/public/config/default_phkg.js @@ -0,0 +1,407 @@ +/** @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); + }); +} diff --git a/platform/ui-next/src/components/SidePanel/SidePanel.tsx b/platform/ui-next/src/components/SidePanel/SidePanel.tsx index 5a5e457..c949bc7 100644 --- a/platform/ui-next/src/components/SidePanel/SidePanel.tsx +++ b/platform/ui-next/src/components/SidePanel/SidePanel.tsx @@ -341,7 +341,8 @@ const SidePanel = ({ fetchAccessionNumber(); }, [studyInstanceUID]); // Run when studyInstanceUID changes - // Ubah fungsi fetchExpertiseData menjadi dengan parameter accessionNumber + const [expertiseError, setExpertiseError] = useState(null); + const fetchExpertiseData = async accessionNumber => { try { // Check if window.config.expertise_host exists @@ -355,46 +356,45 @@ const SidePanel = ({ return; } - setIsExpertiseLoading(true); - const url = `${window.config.expertise_host}/nv/query.php?method=view&AccessionNumber=${encodeURIComponent(accessionNumber)}`; + accessionNumber = 'MR.251027.001'; - // Debuggging - // const url = 'http://152.42.173.210/nv/testQueryBase64.php'; + setIsExpertiseLoading(true); + const url = `${window.config.expertise_host}/${encodeURIComponent(accessionNumber)}`; const response = await fetch(url); - const data = await response.json(); - console.log('Study data:', data); + const resp = await response.json(); + console.log('Expertise Resp:', resp); - if (data?.study?.expertise && data.study.expertise.length > 0) { - const expertiseItem = { ...data.study.expertise[0] }; + if (resp?.error) { + setExpertiseError(resp.message || resp.error); + return; + } + + if (resp?.status_code === 200 && resp?.data) { + const expertiseItem = { ...resp.data }; // Decode base64 encoded fields - // UTF-8 safe base64 decoding const decodeBase64 = str => { try { - // Step 1: decode base64 to binary const binary = atob(str); - // Step 2: create a Uint8Array from the binary string const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } - // Step 3: decode the Uint8Array as UTF-8 return new TextDecoder('utf-8').decode(bytes); } catch (e) { console.error('Error decoding base64 string:', e); - return str; // Return original if decoding fails + return str; } }; // Decode known base64 fields - if (expertiseItem.expertise) { - expertiseItem.expertise = decodeBase64(expertiseItem.expertise); + if (expertiseItem.ekspertiseResult) { + expertiseItem.ekspertiseResult = decodeBase64(expertiseItem.ekspertiseResult); } if (expertiseItem.radiologist) { expertiseItem.radiologist = decodeBase64(expertiseItem.radiologist); } - // Add any other fields that might be base64 encoded setExpertiseData(expertiseItem); } @@ -416,83 +416,45 @@ const SidePanel = ({ ); } + if (expertiseError) { + return ( + +

Expertise

+

{expertiseError}

+
+ ); + } + if (!expertiseData) { 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 const formattedData = [ - { label: 'Dokter Pengirim', value: expertiseData.ordering_physician || '' }, + { label: 'Dokter Pengirim', value: expertiseData.orderingPhysician || '' }, { label: 'Dokter Radiologis', value: expertiseData.radiologist || '' }, - { label: 'Waktu Expertise', value: expertiseData.expertise_dttm || '' }, + { label: 'Waktu Expertise', value: expertiseData.expertiseDttm || '' }, ]; - // Add additional sections from parsed text - Object.entries(parsedSections).forEach(([key, value]) => { - formattedData.push({ - label: key, - value: Array.isArray(value) ? value : [value], - }); - }); - return ( - -

Expertise

+ +

Expertise

{formattedData.map((section, index) => (
-
{section.label}:
- {Array.isArray(section.value) ? ( -
    - {section.value.map((item, idx) => ( -
  • {item}
  • - ))} -
- ) : ( -

{section.value}

- )} +
{section.label}:
+

{section.value}

))} +
+
Hasil Expertise:
+
+
); };