8 Commits

Author SHA1 Message Date
mario
3a2f54989f simplify config 2025-05-19 11:57:27 +07:00
mario
18d5b6dd9a add: shortlink DoB auth page 2025-05-13 16:15:37 +07:00
mario
7cad1c5e05 prevent patient to see Worklist 2025-05-13 08:52:45 +07:00
mario
7f4548e18c add: Login page and route 2025-05-13 08:51:38 +07:00
mario
eaa18b8389 edit: patch XHR Request dengan coverage lebih luas dari monkeyPatchXML 2025-05-09 16:58:04 +07:00
86ad0b38dd Monkey Patch XMLHttpRequest -- inject bearer token and verify response 2025-04-29 09:42:53 +07:00
cb380a521d default use cloud pacs 2025-04-29 08:44:52 +07:00
mario
5f56d06fcd edit ip pdf dan cloud 2025-04-26 23:38:18 +07:00
32 changed files with 553 additions and 830 deletions

3
.gitignore vendored
View File

@@ -12,6 +12,7 @@ coverage/
.yarn/
.nx/
addOns/yarn.lock
**.zip
# YALC (for Erik)
.yalc
@@ -59,3 +60,5 @@ tests/playwright-report/
# Dummy
/dump
jwt-auth-inject.json
platform/app/dist.zip

View File

@@ -1,104 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

View File

@@ -1,11 +0,0 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"trailingComma": "es5",
"printWidth": 100,
"proseWrap": "always",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"arrowParens": "avoid",
"endOfLine": "auto"
}

View File

@@ -1,96 +0,0 @@
const path = require('path');
const pkg = require('../package.json');
const outputFile = 'index.umd.js';
const rootDir = path.resolve(__dirname, '../');
const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`);
// Todo: add ESM build for the extension in addition to umd build
const config = {
mode: 'production',
entry: rootDir + '/' + pkg.module,
devtool: 'source-map',
output: {
path: outputFolder,
filename: outputFile,
library: pkg.name,
libraryTarget: 'umd',
chunkFilename: '[name].chunk.js',
umdNamedDefine: true,
globalObject: "typeof self !== 'undefined' ? self : this",
},
externals: [
{
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
'@ohif/core': {
commonjs2: '@ohif/core',
commonjs: '@ohif/core',
amd: '@ohif/core',
root: '@ohif/core',
},
'@ohif/ui': {
commonjs2: '@ohif/ui',
commonjs: '@ohif/ui',
amd: '@ohif/ui',
root: '@ohif/ui',
},
},
],
module: {
rules: [
{
test: /\.svg?$/,
oneOf: [
{
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false
},
},
},
]
},
prettier: false,
svgo: true,
titleProp: true,
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
],
},
{
test: /(\.jsx|\.js|\.tsx|\.ts)$/,
loader: 'babel-loader',
exclude: /(node_modules|bower_components)/,
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
],
},
resolve: {
modules: [path.resolve('./node_modules'), path.resolve('./src')],
extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'],
},
};
module.exports = config;

View File

@@ -1,9 +0,0 @@
MIT License
Copyright (c) 2025 expertise-panel ()
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,7 +0,0 @@
# expertise-panel
## Description
Expertisi Panel extension
## Author
## License
MIT

View File

@@ -1,50 +0,0 @@
module.exports = {
plugins: [
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-transform-typescript',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
],
env: {
test: {
presets: [
[
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
'@babel/preset-env',
{
modules: 'commonjs',
debug: false,
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
],
},
production: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: ['react-refresh/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};

View File

@@ -1,75 +0,0 @@
{
"name": "expertise-panel",
"version": "0.0.1",
"description": "Expertisi Panel extension",
"author": "",
"license": "MIT",
"main": "dist/umd/expertise-panel/index.umd.js",
"files": [
"dist/**",
"public/**",
"README.md"
],
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"module": "src/index.tsx",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:my-extension": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "^3.9.1",
"@ohif/extension-default": "^3.9.1",
"@ohif/extension-cornerstone": "^3.9.1",
"@ohif/i18n": "^1.0.0",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^12.2.2",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1",
"webpack": "5.89.0",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime": "^7.20.13"
},
"devDependencies": {
"@babel/core": "7.24.7",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0",
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"babel-eslint": "9.x",
"babel-loader": "^8.2.4",
"@svgr/webpack": "^8.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.0",
"cross-env": "^7.0.3",
"dotenv": "^14.1.0",
"eslint": "^8.39.0",
"eslint-loader": "^2.0.0",
"webpack": "5.89.0",
"webpack-merge": "^5.7.3",
"webpack-cli": "^5.0.2"
}
}

View File

@@ -1,7 +0,0 @@
import React from 'react';
function ExpertiseSidePanelComponent() {
return <div className="w-full text-center text-white">Hello Worlds</div>;
}
export default ExpertiseSidePanelComponent;

View File

@@ -1,5 +0,0 @@
import packageJson from '../package.json';
const id = packageJson.name;
export { id };

View File

@@ -1,97 +0,0 @@
import ExpertiseSidePanelComponent from './ExpertiseSidePanelComponent';
import { id } from './id';
/**
* You can remove any of the following modules if you don't need them.
*/
export default {
/**
* Only required property. Should be a unique value across all extensions.
* You ID can be anything you want, but it should be unique.
*/
id,
/**
* Perform any pre-registration tasks here. This is called before the extension
* is registered. Usually we run tasks such as: configuring the libraries
* (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
* this extension is providing.
*/
preRegistration: ({ servicesManager, commandsManager, configuration = {} }) => {},
/**
* PanelModule should provide a list of panels that will be available in OHIF
* for Modes to consume and render. Each panel is defined by a {name,
* iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/
getPanelModule: ({ servicesManager, commandsManager, extensionManager }) => {
return [
{
name: 'ExpertiseSidePanel',
iconName: 'logo-ohif-small',
iconLabel: 'Expertise Side Panel',
label: 'Expertise Side Panel Label',
component: ExpertiseSidePanelComponent,
},
];
},
/**
* ViewportModule should provide a list of viewports that will be available in OHIF
* for Modes to consume and use in the viewports. Each viewport is defined by
* {name, component} object. Example of a viewport module is the CornerstoneViewport
* that is provided by the Cornerstone extension in OHIF.
*/
getViewportModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* ToolbarModule should provide a list of tool buttons that will be available in OHIF
* for Modes to consume and use in the toolbar. Each tool button is defined by
* {name, defaultComponent, clickHandler }. Examples include radioGroupIcons and
* splitButton toolButton that the default extension is providing.
*/
getToolbarModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* LayoutTemplateMOdule should provide a list of layout templates that will be
* available in OHIF for Modes to consume and use to layout the viewer.
* Each layout template is defined by a { name, id, component}. Examples include
* the default layout template provided by the default extension which renders
* a Header, left and right sidebars, and a viewport section in the middle
* of the viewer.
*/
getLayoutTemplateModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* SopClassHandlerModule should provide a list of sop class handlers that will be
* available in OHIF for Modes to consume and use to create displaySets from Series.
* Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}.
* Examples include the default sop class handler provided by the default extension
*/
getSopClassHandlerModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* HangingProtocolModule should provide a list of hanging protocols that will be
* available in OHIF for Modes to use to decide on the structure of the viewports
* and also the series that hung in the viewports. Each hanging protocol is defined by
* { name, protocols}. Examples include the default hanging protocol provided by
* the default extension that shows 2x2 viewports.
*/
getHangingProtocolModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* CommandsModule should provide a list of commands that will be available in OHIF
* for Modes to consume and use in the viewports. Each command is defined by
* an object of { actions, definitions, defaultContext } where actions is an
* object of functions, definitions is an object of available commands, their
* options, and defaultContext is the default context for the command to run against.
*/
getCommandsModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* ContextModule should provide a list of context that will be available in OHIF
* and will be provided to the Modes. A context is a state that is shared OHIF.
* Context is defined by an object of { name, context, provider }. Examples include
* the measurementTracking context provided by the measurementTracking extension.
*/
getContextModule: ({ servicesManager, commandsManager, extensionManager }) => {},
/**
* DataSourceModule should provide a list of data sources to be used in OHIF.
* DataSources can be used to map the external data formats to the OHIF's
* native format. DataSources are defined by an object of { name, type, createDataSource }.
*/
getDataSourcesModule: ({ servicesManager, commandsManager, extensionManager }) => {},
};

View File

@@ -102,8 +102,20 @@ export const studyDataForOverlayItem = (studyInstanceUID: string) => {
try {
const qidoRootUrl = getQidoRootUrl();
// Get the authentication token from session storage
const authToken = window.sessionStorage.getItem('ohif-auth-token');
// Create request headers with Authorization if token exists
const headers: HeadersInit = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(
`${qidoRootUrl}/studies?limit=101&offset=0&fuzzymatching=false&includefield=00080050,00081030,00101010,0010004&StudyInstanceUID=${studyInstanceUID}`
`${qidoRootUrl}/studies?limit=101&offset=0&fuzzymatching=false&includefield=00080050,00081030,00101010,0010004&StudyInstanceUID=${studyInstanceUID}`,
{
headers,
}
);
if (!response.ok) {

View File

@@ -39,6 +39,15 @@ export default function initWADOImageLoader(
Accept: acceptHeader,
};
// // Patch Mario:
const authToken = sessionStorage.getItem('ohif-auth-token');
if (!authToken) {
window.location.href = '/login';
return;
}
xhrRequestHeaders.Authorization = `Bearer ${authToken}`;
if (headers) {
Object.assign(xhrRequestHeaders, headers);
}

View File

@@ -26,6 +26,15 @@ const CornerstoneViewportDownloadForm = ({
const activeViewportElement = enabledElement?.element;
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
// console.log('cornerstoneViewportService', cornerstoneViewportService);
const viewportInfo = cornerstoneViewportService.getViewportInfo("default");
// console.log('viewportInfo', viewportInfo);
// Retrieve StudyInstanceUID from viewportInfo
const StudyInstanceUID = viewportInfo.getViewportData().data[0].StudyInstanceUID;
const SetInstanceUID = viewportInfo.getViewportData().data[0].displaySetInstanceUID;
// console.log('StudyInstanceUID', StudyInstanceUID);
// console.log('SetInstanceUID', SetInstanceUID);
const {
viewportId: activeViewportId,
renderingEngineId,
@@ -108,6 +117,10 @@ const CornerstoneViewportDownloadForm = ({
const downloadCanvas = getOrCreateCanvas(element);
// Log the canvas content before conversion
const context = downloadCanvas.getContext('2d');
const imageData = context.getImageData(0, 0, downloadCanvas.width, downloadCanvas.height);
const type = 'image/' + fileType;
const dataUrl = downloadCanvas.toDataURL(type, 1);
@@ -214,6 +227,43 @@ const CornerstoneViewportDownloadForm = ({
});
};
// New function to send annotation data
const sendAnnotationData = async (base64Image, activeViewportElement) => {
try {
// Get the SOPInstanceUID from the active viewport
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
const imageId = activeViewportEnabledElement?.viewport?.getCurrentImageId();
if (!base64Image || !StudyInstanceUID) {
throw new Error('Missing required data');
}
const payload = {
image: base64Image,
StudyInstanceUID: StudyInstanceUID
};
const response = await fetch('http://host:port/one-api/tools/annotation/store', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
console.log('Annotation data sent successfully:', data);
return data;
} catch (error) {
console.error('Error sending annotation data:', error);
throw error;
}
};
const downloadBlob = (filename, fileType) => {
const file = `${filename}.${fileType}`;
const divForDownloadViewport = document.querySelector(
@@ -221,9 +271,16 @@ const CornerstoneViewportDownloadForm = ({
);
html2canvas(divForDownloadViewport).then(canvas => {
const dataUrl = canvas.toDataURL(fileType, 1.0);
const base64Image = dataUrl.split(',')[1]; // Remove prefix 'data:image/png;base64,'
// Send annotation data
sendAnnotationData(base64Image, activeViewportElement)
.catch(error => console.error('Annotation data sending failed:', error));
const link = document.createElement('a');
link.download = file;
link.href = canvas.toDataURL(fileType, 1.0);
link.href = dataUrl;
link.click();
});
};

View File

@@ -71,25 +71,11 @@ const SidePanelWithServices = ({
const activatePanelSubscription = panelService.subscribe(
panelService.EVENTS.ACTIVATE_PANEL,
(activatePanelEvent: Types.ActivatePanelEvent) => {
// Handle the `-exp` suffix logic
const isExpertisePanel = activatePanelEvent.panelId.endsWith('-exp');
const realPanelID = isExpertisePanel
? activatePanelEvent.panelId.replace(/-exp$/, '')
: activatePanelEvent.panelId;
const tabIndex = tabs.findIndex(tab => tab.id === realPanelID);
if (isExpertisePanel && side === 'right') {
const shouldOpen = !sidePanelOpen; // Use sidePanelOpen to determine toggle state
setSidePanelOpen(shouldOpen);
if (shouldOpen) {
setActiveTabIndex(tabIndex !== -1 ? tabIndex : null);
} else {
setActiveTabIndex(null);
if (sidePanelOpen || activatePanelEvent.forceActive) {
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
if (tabIndex !== -1) {
setActiveTabIndex(tabIndex);
}
} else if (tabIndex !== -1) {
setActiveTabIndex(tabIndex);
}
}
);
@@ -109,7 +95,6 @@ const SidePanelWithServices = ({
onClose={handleClose}
onActiveTabIndexChange={handleActiveTabIndexChange}
expandedWidth={expandedWidth}
servicesManager={servicesManager} // Pass servicesManager ke SidePanel
/>
);
};

View File

@@ -85,6 +85,14 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
if (authHeaders && authHeaders.Authorization) {
xhrRequestHeaders.Authorization = authHeaders.Authorization;
}
const authToken = sessionStorage.getItem('ohif-auth-token');
if (!authToken) {
window.location.href = '/login';
return;
}
xhrRequestHeaders.Authorization = `Bearer ${authToken}`;
return xhrRequestHeaders;
};

View File

@@ -6,7 +6,8 @@ function OHIFCornerstonePdfViewport({ displaySets }) {
var [url, setUrl] = useState(null);
const sopInstanceUid = displaySets[0].SOPInstanceUID;
url = `http://128.199.154.150:8080/rid/IHERetrieveDocument?requestType=DOCUMENT&documentUID=${sopInstanceUid}&preferredContentType=application%2Fpdf`;
url = `http://${window.config.pacs_document_host}:${window.config.pacs_document_port}/rid/IHERetrieveDocument?requestType=DOCUMENT&documentUID=${sopInstanceUid}&preferredContentType=application%2Fpdf`;
console.log("URL PDF", url);
useEffect(() => {
document.body.addEventListener('drag', makePdfDropTarget);

View File

@@ -42,8 +42,7 @@
"@ohif/extension-default": "3.9.1",
"@ohif/extension-dicom-pdf": "3.9.1",
"@ohif/extension-dicom-video": "3.9.1",
"@ohif/extension-measurement-tracking": "3.9.1",
"expertise-panel": "^0.0.1"
"@ohif/extension-measurement-tracking": "3.9.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",

View File

@@ -72,7 +72,6 @@ const extensionDependencies = {
'@ohif/extension-cornerstone-dicom-rt': '^3.0.0',
'@ohif/extension-dicom-pdf': '^3.0.1',
'@ohif/extension-dicom-video': '^3.0.1',
'expertise-panel': '^0.0.1',
};
function modeFactory({ modeConfiguration }) {
@@ -182,7 +181,7 @@ function modeFactory({ modeConfiguration }) {
return {
id: ohif.layout,
props: {
leftPanels: [tracked.thumbnailList, 'expertise-panel.panelModule.ExpertiseSidePanel'],
leftPanels: [tracked.thumbnailList],
rightPanels: [cornerstone.segmentation, tracked.measurements],
rightPanelClosed: true,
viewports: [

View File

@@ -25,7 +25,7 @@ const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
const PROXY_PATH_REWRITE_FROM = process.env.PROXY_PATH_REWRITE_FROM;
const PROXY_PATH_REWRITE_TO = process.env.PROXY_PATH_REWRITE_TO;
const OHIF_PORT = Number(process.env.OHIF_PORT || 3000);
const OHIF_PORT = Number(process.env.OHIF_PORT || 3030);
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
const Dotenv = require('dotenv-webpack');
const writePluginImportFile = require('./writePluginImportsFile.js');
@@ -78,10 +78,6 @@ module.exports = (env, argv) => {
// Hoisted Yarn Workspace Modules
path.resolve(__dirname, '../../../node_modules'),
SRC_DIR,
path.resolve(
__dirname,
'/home/mario/Works/ohif-viewer/expertise-panel-ext/expertise-panel/node_modules'
),
],
},
plugins: [

View File

@@ -61,10 +61,6 @@
"packageName": "@ohif/extension-cornerstone-dicom-rt",
"default": false,
"version": "3.0.0"
},
{
"packageName": "expertise-panel",
"version": "0.0.1"
}
],
"modes": [

View File

@@ -1,10 +1,15 @@
/** @type {AppTypes.Config} */
function sas_get_token() {
//implement token here
return '';
}
window.config = {
sasGetToken: sas_get_token,
routerBasename: '/',
// whiteLabeling: {},
extensions: [],
modes: [],
customizationService: {},
showStudyList: true,
// some windows systems have issues with more than 3 web workers
maxNumberOfWebWorkers: 3,
@@ -23,8 +28,9 @@ window.config = {
prefetch: 25,
},
expertise: false, //* Tambahan untuk enable expertise (CustomizableViewportOverlay)
expertise_host: 'http://128.199.154.150', //* Tambahan untuk fetch data Expertise)
expertise_host: `https://devone.aplikasi.web.id/one-api/mockup/pacsmwl/Workorder/get_dummy_expertise`, //* Tambahan untuk fetch data Expertise)
pacs_document_host: `152.42.173.210`,
pacs_document_port: 8080,
// filterQueryParam: false,
// defaultDataSourceName: 'dicomweb',
defaultDataSourceName: 'local-proxy',
@@ -45,8 +51,8 @@ window.config = {
configuration: {
friendlyName: 'Static WADO Local Data',
name: 'DCM4CHEE',
qidoRoot: 'http://128.199.154.150:5000/rs',
wadoRoot: 'http://128.199.154.150:5000/rs',
qidoRoot: `http://152.42.173.210:5000/rs`,
wadoRoot: `http://152.42.173.210:5000/rs`,
qidoSupportsIncludeField: false,
supportsReject: true,
supportsStow: true,
@@ -63,6 +69,7 @@ window.config = {
},
},
},
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy',
sourceName: 'dicomwebproxy',

View File

@@ -1,47 +1,18 @@
/** @type {AppTypes.Config} */
window.config = {
routerBasename: '/',
pacs_document_host: `${window.location.hostname}`,
pacs_document_port: 8080,
expertise: false,
enableGoogleCloudAdapter: false,
// below flag is for performance reasons, but it might not work for all servers
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
// This is an array, but we'll only use the first entry for now
oidc: [
{
// ~ REQUIRED
// Authorization Server URL
authority: 'https://accounts.google.com',
client_id: '382212153306-7q39hdie4ecj0uhemkitvedo93bnvfhn.apps.googleusercontent.com',
redirect_uri: '/callback',
response_type: 'id_token token',
scope:
'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
// ~ OPTIONAL
post_logout_redirect_uri: '/logout-redirect.html',
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
automaticSilentRenew: true,
revokeAccessTokenOnSignout: true,
// Tambahan dari Google CLoud Secret
project_id: "westone-433204",
auth_uri: "https://accounts.google.com/o/oauth2/auth",
token_uri: "https://oauth2.googleapis.com/token",
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
client_secret: "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",
redirect_uris: [
"http://devkedungdoro.aplikasi.web.id:3000/callback"
],
javascript_origins: [
"https://devone.aplikasi.web.id",
"http://devkedungdoro.aplikasi.web.id:3000"
]
},
],
extensions: [],
modes: [],
showStudyList: true,
showStudyList: false,
// filterQueryParam: false,
defaultDataSourceName: 'dicomweb',
dataSources: [
@@ -51,27 +22,18 @@ window.config = {
configuration: {
friendlyName: 'dcmjs DICOMWeb Server',
name: 'GCP',
wadoUriRoot:
'https://healthcare.googleapis.com/v1/projects/westone-433204/locations/asia-southeast2/datasets/sas-dicom-storage/dicomStores/ohif/dicomWeb',
qidoRoot:
'https://healthcare.googleapis.com/v1/projects/westone-433204/locations/asia-southeast2/datasets/sas-dicom-storage/dicomStores/ohif/dicomWeb',
wadoRoot:
'https://healthcare.googleapis.com/v1/projects/westone-433204/locations/asia-southeast2/datasets/sas-dicom-storage/dicomStores/ohif/dicomWeb',
wadoUriRoot: `http://${window.location.hostname}:5555/dicomWeb`,
qidoRoot: `http://${window.location.hostname}:5555/dicomWeb`,
wadoRoot: `http://${window.location.hostname}:5555/dicomWeb`,
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: true,
supportsFuzzyMatching: false,
supportsWildcard: true,
dicomUploadEnabled: true,
dicomUploadEnabled: false,
omitQuotationForMultipartRequest: true,
configurationAPI: 'ohif.dataSourceConfigurationAPI.google',
defaultDicomStoreConfiguredItems: {
id: 'projects/westone-433204/locations/asia-southeast2/datasets/sas-dicom-storage',
itemType: '3',
name: 'ohif',
url: 'https://healthcare.googleapis.com/v1/projects/westone-433204/locations/asia-southeast2/datasets/sas-dicom-storage/dicomStores/ohif'
},
},
},
{

View File

@@ -36,6 +36,49 @@ import createRoutes from './routes';
import appInit from './appInit.js';
import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes';
import { ShepherdJourneyProvider } from 'react-shepherd';
import { initializeCustomAuth } from './utils/initUserAuthenticationService';
function injectAuth() {
console.log('---> Inject Auth');
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
// Kalau ingin disable study list (Role Patient)
// window.config.showStudyList = false;
const authToken = sessionStorage.getItem('ohif-auth-token');
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
this._url = url; // Save URL if you want conditional logic
return originalXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
this.setRequestHeader('Authorization', `Bearer ${authToken}`);
this.addEventListener('readystatechange', function () {
if (this.readyState === 4) {
// readyState 4 = DONE
try {
// Check for auth errors (401/403) and redirect to login if needed
if (this.status === 401 || this.status === 403) {
window.sessionStorage.removeItem('ohif-auth-token');
window.location.href = '/login';
}
} catch (e) {
console.error('Error handling auth response:', e);
}
}
});
return originalXHRSend.apply(this, arguments);
};
}
// Setup token access function
window.config.sasGetToken = () => window.sessionStorage.getItem('ohif-auth-token');
// Enable auth token injection
// injectAuth();
let commandsManager: CommandsManager,
extensionManager: ExtensionManager,
@@ -108,6 +151,9 @@ function App({
customizationService,
} = servicesManager.services;
// Initialize our custom authentication service
initializeCustomAuth(userAuthenticationService);
const providers = [
[AppConfigProvider, { value: appConfigState }],
[UserAuthenticationProvider, { service: userAuthenticationService }],

View File

@@ -0,0 +1,129 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useUserAuthentication } from '@ohif/ui';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const [, authContext] = useUserAuthentication();
// Get the intended destination from URL query params or default to home
const searchParams = new URLSearchParams(location.search);
const redirectPath = searchParams.get('redirect') || '/';
// Check if already authenticated
useEffect(() => {
const token = window.sessionStorage.getItem('ohif-auth-token');
if (token) {
// Already logged in, redirect to destination
navigate(redirectPath, { replace: true });
}
}, [redirectPath, navigate]);
const handleLogin = async e => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
// Use window.config.goProxyHost for authentication endpoint
const proxyHost = window.config?.goProxyHost || `http://${window.location.hostname}:5555`;
const authEndpoint = `${proxyHost}/auth/login`;
// Call go-ohif-proxy login endpoint
const response = await fetch(authEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed. Please check your credentials.');
}
const data = await response.json();
// Store token in sessionStorage
window.sessionStorage.setItem('ohif-auth-token', data.access_token);
// Decode token to extract role and user information
let userInfo = data.user;
// Update the auth context
authContext.setUser({
...userInfo,
token: data.access_token,
});
// Set window.config.sasGetToken for the injectAuth function
if (window.config) {
window.config.sasGetToken = () => window.sessionStorage.getItem('ohif-auth-token');
}
// Handle role-specific redirects if specified in response
if (data.redirect_url) {
navigate(data.redirect_url, { replace: true });
} else {
// Redirect to the original destination
navigate(redirectPath, { replace: true });
}
} catch (error) {
console.error('Login error:', error);
setError(error.message || 'Failed to log in. Please try again.');
} finally {
setIsLoading(false);
}
};
return (
<div className="flex h-screen w-screen items-center justify-center bg-black">
<div className="bg-primary-dark w-96 rounded p-8 shadow-lg">
<h1 className="mb-8 text-center text-2xl font-bold text-white">Login to OHIF Viewer</h1>
{error && <div className="mb-4 rounded bg-red-800 px-4 py-2 text-white">{error}</div>}
<form onSubmit={handleLogin}>
<div className="mb-4">
<label className="mb-2 block text-sm font-bold text-white">Email</label>
<input
type="text"
className="focus:shadow-outline w-full appearance-none rounded border py-2 px-3 leading-tight text-gray-700 shadow focus:outline-none"
value={email}
onChange={e => setEmail(e.target.value)}
required
/>
</div>
<div className="mb-6">
<label className="mb-2 block text-sm font-bold text-white">Password</label>
<input
type="password"
className="focus:shadow-outline w-full appearance-none rounded border py-2 px-3 leading-tight text-gray-700 shadow focus:outline-none"
value={password}
onChange={e => setPassword(e.target.value)}
required
/>
</div>
<div className="flex items-center justify-center">
<button
type="submit"
className="focus:shadow-outline w-full rounded bg-blue-500 py-2 px-4 font-bold text-white hover:bg-blue-700 focus:outline-none"
disabled={isLoading}
>
{isLoading ? 'Logging in...' : 'Log In'}
</button>
</div>
</form>
</div>
</div>
);
};
export default Login;

View File

@@ -0,0 +1,131 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useUserAuthentication } from '@ohif/ui';
const ShortlinkLogin = () => {
const [dob, setDob] = useState('');
const [shortToken, setShortToken] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const [, authContext] = useUserAuthentication();
// Parse the short token from URL query params
useEffect(() => {
const searchParams = new URLSearchParams(location.search);
const token = searchParams.get('short');
if (token) {
setShortToken(token);
} else {
// No short token found, redirect to regular login
setError('No shortlink token found in URL');
setTimeout(() => {
navigate('/login', { replace: true });
}, 3000);
}
}, [location.search, navigate]);
// Handle form submission
const handleSubmit = async e => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
// Use window.config.goProxyHost for authentication endpoint
const proxyHost = window.config?.goProxyHost || `http://${window.location.hostname}:5555`;
const authEndpoint = `${proxyHost}/auth/shortlink`;
// Call the shortlink authentication endpoint
const response = await fetch(authEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ short_token: shortToken, dob }),
});
if (!response.ok) {
throw new Error('Authentication failed. Please check your date of birth and try again.');
}
const data = await response.json();
// Store token in sessionStorage
window.sessionStorage.setItem('ohif-auth-token', data.access_token);
// Decode token to extract user information (if available in token)
let userInfo = data.user;
// Update the auth context
authContext.setUser({
...userInfo,
token: data.access_token,
});
// Set window.config.sasGetToken for the injectAuth function
if (window.config) {
window.config.sasGetToken = () => window.sessionStorage.getItem('ohif-auth-token');
}
// Navigate to the viewer page with the authenticated patient's study
// The actual URL would depend on how studies are loaded in your OHIF instance
if (data.redirect_url) {
navigate(data.redirect_url, { replace: true });
} else {
// Default navigation if no specific redirect is provided
navigate('/', { replace: true });
}
} catch (error) {
console.error('Authentication error:', error);
setError(error.message || 'Failed to authenticate. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleDateChange = e => {
// Format date input as YYYY-MM-DD
setDob(e.target.value);
};
return (
<div className="flex h-screen w-screen items-center justify-center bg-black">
<div className="bg-primary-dark w-96 rounded p-8 shadow-lg">
<h1 className="mb-8 text-center text-2xl font-bold text-white">Patient Access</h1>
{error && <div className="mb-4 rounded bg-red-800 px-4 py-2 text-white">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="mb-6">
<label className="mb-2 block text-sm font-bold text-white">
Please enter your date of birth
</label>
<input
type="date"
className="focus:shadow-outline w-full appearance-none rounded border py-2 px-3 leading-tight text-gray-700 shadow focus:outline-none"
value={dob}
onChange={handleDateChange}
required
/>
<p className="mt-1 text-xs text-gray-400">Format: Bulan - Tanggal - Tahun</p>
</div>
<div className="flex items-center justify-center">
<button
type="submit"
className="focus:shadow-outline w-full rounded bg-blue-500 py-2 px-4 font-bold text-white hover:bg-blue-700 focus:outline-none"
disabled={isLoading || !shortToken}
>
{isLoading ? 'Verifying...' : 'Access My Images'}
</button>
</div>
</form>
</div>
</div>
);
};
export default ShortlinkLogin;

View File

@@ -85,6 +85,38 @@ function WorkList({
const debouncedFilterValues = useDebounce(filterValues, 200);
const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues;
/*
* Patch untuk Role checking patient gabisa akses ke study list
*/
const token = window.sessionStorage.getItem('ohif-auth-token');
if (!token) {
return;
}
const decodedToken = decodeToken(token);
// Check jika 'role' = 'patient' tapi akses '/' return ke viewer
if (decodedToken && decodedToken.role === 'patient') {
const currentPath = window.location.pathname + window.location.search;
if (currentPath === '/') {
console.log(
'User is a patient and trying to access the root path. Redirecting to his/her home URL.'
);
window.location.href = `${decodedToken.home_url}`;
}
}
function decodeToken(token) {
try {
const payload = token.split('.')[1];
if (payload) {
return JSON.parse(atob(payload));
}
} catch (e) {
console.error('Error parsing JWT token', e);
}
return null;
}
/*
* The default sort value keep the filters synchronized with runtime conditional sorting
* Only applied if no other sorting is specified and there are less than 101 studies
@@ -542,7 +574,7 @@ function WorkList({
/>
<Onboarding />
<InvestigationalUseDialog dialogConfiguration={appConfig?.investigationalUseDialog} />
<div className="flex flex-col h-full overflow-y-auto">
<div className="flex h-full flex-col overflow-y-auto">
<ScrollArea>
<div className="flex grow flex-col">
<StudyListFilter
@@ -558,9 +590,7 @@ function WorkList({
// ? () => dataSourceConfigurationComponent()
// : undefined
// }
getDataSourceConfigurationComponent={
undefined
}
getDataSourceConfigurationComponent={undefined}
/>
</div>
{hasStudies ? (

View File

@@ -12,6 +12,8 @@ import buildModeRoutes from './buildModeRoutes';
import PrivateRoute from './PrivateRoute';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Login from './Login';
import ShortlinkLogin from './ShortlinkLogin';
const NotFoundServer = ({
message = 'Unable to query for studies at this time. Check your data source configuration or network connection',
@@ -74,6 +76,15 @@ const bakedInRoutes = [
path: '/localbasic',
children: Local.bind(null, { modePath: 'viewer/dicomlocal' }),
},
// * Custom Patch untuk Login go-ohif-proxy
{
path: '/login',
children: Login,
},
{
path: '/short-auth',
children: ShortlinkLogin,
},
];
// NOT FOUND (404)

View File

@@ -0,0 +1,72 @@
/**
* Initializes the custom authentication service for OHIF Viewer
* to work with go-ohif-proxy authentication
*/
export function initializeCustomAuth(userAuthenticationService) {
// Set up the authentication service with custom implementation
userAuthenticationService.setServiceImplementation({
// Custom implementation to handle unauthenticated users
handleUnauthenticated: () => {
// Check if there's a shortlink token in the URL
const urlParams = new URLSearchParams(window.location.search);
const shortToken = urlParams.get('short');
// If there's a shortlink token, redirect to the shortlink login page
if (shortToken) {
window.location.href = `/short-auth?short=${shortToken}`;
return null;
}
// Otherwise, handle as normal login flow
// Get the current path for redirect after login
const currentPath = window.location.pathname + window.location.search;
// Clear any existing tokens
window.sessionStorage.removeItem('ohif-auth-token');
// Redirect to login page with the redirect URL in query params
window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`;
// Return null to prevent rendering while redirecting
return null;
},
// Custom implementation to get the authorization header
// di ohif3.9.1 ini sepertinya masih development
// getAuthorizationHeader: () => {
// const token = window.sessionStorage.getItem('ohif-auth-token');
// return token ? `Bearer ${token}` : undefined;
// },
});
// Set authentication as enabled
userAuthenticationService.set({ enabled: true });
// Check if we already have a token and set the user if we do
const token = window.sessionStorage.getItem('ohif-auth-token');
if (!token) {
return;
}
const decodedToken = decodeToken(token);
// Check jika 'role' = 'patient' tapi akses '/' return ke viewer
if (decodedToken && decodedToken.role === 'patient') {
const currentPath = window.location.pathname + window.location.search;
if (currentPath === '/') {
console.log('User is a patient and trying to access the root path. Redirecting to /patient.');
window.location.href = `${decodedToken.home_url}`;
}
}
function decodeToken(token) {
try {
const payload = token.split('.')[1];
if (payload) {
return JSON.parse(atob(payload));
}
} catch (e) {
console.error('Error parsing JWT token', e);
}
return null;
}
}

View File

@@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Icons } from '../Icons';
import { TooltipTrigger, TooltipContent, TooltipProvider, Tooltip } from '../Tooltip';
import { Separator } from '../Separator';
import { ScrollArea } from '../ScrollArea';
type StyleMap = {
open: {
@@ -153,22 +152,10 @@ const SidePanel = ({
onClose,
expandedWidth = 280,
onActiveTabIndexChange,
servicesManager, // Tambah servicesManager as a prop
}) => {
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
const [activeTabIndex, setActiveTabIndex] = useState(0);
const [isExpertiseVisible, setIsExpertiseVisible] = useState(false); // New state for expertise visibility
const [expertiseData, setExpertiseData] = useState(null);
const [isExpertiseLoading, setIsExpertiseLoading] = useState(false);
const { cornerstoneViewportService } = servicesManager.services;
const [viewportData, setViewportData] = useState(null);
// Harusnya (viewportId), tapi karena gabutuh perubahan viewport maka dihardcode 'default'
const viewportInfo = cornerstoneViewportService.getViewportInfo('default');
const studyInstanceUID = viewportInfo?.viewportData?.data?.[0]?.StudyInstanceUID || '';
const styleMap = createStyleMap(expandedWidth, borderSize, collapsedWidth);
const baseStyle = createBaseStyle(expandedWidth);
const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding;
@@ -209,16 +196,6 @@ const SidePanel = ({
updateActiveTabIndex(activeTabIndexProp);
}, [activeTabIndexProp, updateActiveTabIndex]);
const toggleExpertiseVisibility = () => {
const shouldOpenExpertise = !isExpertiseVisible;
setIsExpertiseVisible(shouldOpenExpertise);
// Open the side panel if the expertise panel is being shown
if (shouldOpenExpertise && !panelOpen) {
updatePanelOpen(true);
}
};
const getCloseStateComponent = () => {
const _childComponents = Array.isArray(tabs) ? tabs : [tabs];
return (
@@ -278,230 +255,6 @@ const SidePanel = ({
);
};
// Tambahkan di atas useEffect fetchExpertiseData
useEffect(() => {
const fetchAccessionNumber = async () => {
if (!studyInstanceUID) {
console.warn('No StudyInstanceUID available');
return;
}
try {
const qidoRootUrl = getQidoRootUrl();
if (!qidoRootUrl) {
console.warn('QIDO root URL not configured');
return;
}
// Fetch data with specific fields including Accession Number
const response = await fetch(
`${qidoRootUrl}/studies?includefield=00080050&StudyInstanceUID=${studyInstanceUID}`
);
if (!response.ok) {
throw new Error('Failed to fetch study data');
}
const data = await response.json();
if (data && data.length > 0) {
// Extract accession number from DICOM tag 00080050
const accessionNumber = data[0]['00080050']?.Value?.[0] || '';
// If we have an accession number, call fetchExpertiseData
if (accessionNumber) {
console.log('Found Accession Number:', accessionNumber);
fetchExpertiseData(accessionNumber);
} else {
console.warn('Accession number not found in study data');
}
} else {
console.warn('No study data returned');
}
} catch (error) {
console.error('Error fetching accession number:', error);
}
};
// Helper function to get QIDO root URL
const getQidoRootUrl = () => {
const { config } = window;
if (!config?.dataSources || !config.defaultDataSourceName) {
return null;
}
const dataSource = config.dataSources.find(
ds => ds.sourceName === config.defaultDataSourceName
);
return dataSource?.configuration?.qidoRoot;
};
fetchAccessionNumber();
}, [studyInstanceUID]); // Run when studyInstanceUID changes
// Ubah fungsi fetchExpertiseData menjadi dengan parameter accessionNumber
const fetchExpertiseData = async accessionNumber => {
try {
// Check if window.config.expertise_host exists
if (!window.config?.expertise_host) {
console.warn('Expertise host not configured in window.config.expertise_host');
return;
}
if (!accessionNumber) {
console.warn('No accession number available for expertise lookup');
return;
}
setIsExpertiseLoading(true);
const url = `${window.config.expertise_host}/nv/query.php?method=view&AccessionNumber=${encodeURIComponent(accessionNumber)}`;
const response = await fetch(url);
const data = await response.json();
// console.log('Study data:', data);
const data = {
study: {
accession_no: 'CR.250411.001',
study_iuid: '1.2.826.0.1.3680043.9.7307.1.20250411001',
study_description: '',
study_datetime: '20250411093937',
number_of_series: '1',
number_of_instances: '1',
modality: 'CR',
patient_mrn: '00000941',
patient_name: 'NEFANNY RIDWAN',
patient_sex: 'F',
patient_date_of_birth: '19881127',
patient_age: '36Y 4M 14D',
expertise: [
{
expertise:
'Keterangan : MCU\r\n\r\nRadiografi Thorax PA (inspirasi kurang)\r\n\r\nCor : besar dan bentuk normal\r\nPulmo : tak tampak infiltrat\r\nTrachea tampak di tengah\r\nSinus phrenicocostalis kanan kiri tajam\r\nHemidiafragma kanan kiri tampak baik\r\nTulang-tulang tampak baik\r\nSoft tissue tak tampak kelainan\r\n\r\nKesan :\r\nTidak tampak kelainan signifikan pada pemeriksaan saat ini\r\n\r\nBTK,',
radiologist: 'dr. Hendra Boy Situmorang, Sp.Rad ',
expertise_dttm: '2025-04-11 09:43',
radiologist_edit: null,
expertise_edit_dttm: '0000-00-00 00:00',
ordering_physician: 'dr. Laksmitasari Dewi',
},
],
series: [
{
series_number: '1',
series_iuid: '1.2.156.112536.2.560.28134011043131122.1519098341436.1',
series_description: 'V04_0014',
number_of_instances: 1,
thumbnail:
'http://192.168.22.3/nv/wado_proxy_thumb.php?requestType=WADO&studyUID=1.2.826.0.1.3680043.9.7307.1.20250411001&seriesUID=1.2.156.112536.2.560.28134011043131122.1519098341436.1&objectUID=1.2.156.112536.2.560.28134011043131122.1519098341436.4&rows=123',
sop_iuids: ['1.2.156.112536.2.560.28134011043131122.1519098341436.4'],
},
],
},
};
if (data?.study?.expertise && data.study.expertise.length > 0) {
setExpertiseData(data.study.expertise[0]);
}
} catch (error) {
console.error('Error fetching expertise data:', error);
} finally {
setIsExpertiseLoading(false);
}
};
const getExpertisePanel = () => {
if (side !== 'right') return null; // Only show in the right side panel
if (isExpertiseLoading) {
return (
<div className="flex h-[500px] w-[350px] items-center justify-center text-white">
Loading expertise data...
</div>
);
}
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 Radiologis', value: expertiseData.radiologist || '' },
{ 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 (
<ScrollArea className="border-input bg-background h-[500px] w-[350px] rounded-md border p-2 text-sm text-white">
<h3 className="mb-4 text-lg font-bold">Expertise</h3>
{formattedData.map((section, index) => (
<div
key={index}
className="mb-4"
>
<h5 className="text-base font-bold">{section.label}:</h5>
{Array.isArray(section.value) ? (
<ul className="list-disc pl-6">
{section.value.map((item, idx) => (
<li key={idx}>{item}</li>
))}
</ul>
) : (
<p>{section.value}</p>
)}
</div>
))}
</ScrollArea>
);
};
const getCloseIcon = () => {
return (
<div
@@ -633,7 +386,6 @@ const SidePanel = ({
}
return null;
})}
{getExpertisePanel()} {/* Add expertise panel here */}
</>
) : (
<React.Fragment>{getCloseStateComponent()}</React.Fragment>
@@ -661,7 +413,6 @@ SidePanel.propTypes = {
onClose: PropTypes.func,
onActiveTabIndexChange: PropTypes.func,
expandedWidth: PropTypes.number,
servicesManager: PropTypes.object.isRequired, // Tambah servicesManager prop
};
export { SidePanel };

View File

@@ -62,7 +62,6 @@ const StudyBrowser = ({
data-cy="thumbnail-list"
viewPreset={viewPreset}
onThumbnailContextMenu={onThumbnailContextMenu}
servicesManager={servicesManager} // Pass servicesManager ke Study Item
/>
</React.Fragment>
);

View File

@@ -20,7 +20,6 @@ const StudyItem = ({
onClickUntrack,
viewPreset = 'thumbnails',
onThumbnailContextMenu,
servicesManager, // Tambah servicesManager as a prop
}: withAppTypes) => {
return (
<Accordion
@@ -56,32 +55,15 @@ const StudyItem = ({
}}
>
{isExpanded && displaySets && (
<>
{/* Expertise Button */}
<div
className="bg-primary-dark hover:bg-primary-active my-4 w-full cursor-pointer border border-white py-2 text-center text-white"
onClick={() => {
// Trigger the expertise panel in the right side panel (segmentation Panel)
servicesManager.services.panelService.activatePanel(
'@ohif/extension-cornerstone.panelModule.panelSegmentation-exp',
true
);
}}
>
View Expertise
</div>
{/* Thumbnails */}
<ThumbnailList
thumbnails={displaySets}
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
onThumbnailClick={onClickThumbnail}
onThumbnailDoubleClick={onDoubleClickThumbnail}
onClickUntrack={onClickUntrack}
viewPreset={viewPreset}
onThumbnailContextMenu={onThumbnailContextMenu}
/>
</>
<ThumbnailList
thumbnails={displaySets}
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
onThumbnailClick={onClickThumbnail}
onThumbnailDoubleClick={onDoubleClickThumbnail}
onClickUntrack={onClickUntrack}
viewPreset={viewPreset}
onThumbnailContextMenu={onThumbnailContextMenu}
/>
)}
</AccordionContent>
</AccordionItem>
@@ -104,7 +86,6 @@ StudyItem.propTypes = {
onDoubleClickThumbnail: PropTypes.func,
onClickUntrack: PropTypes.func,
viewPreset: PropTypes.string,
servicesManager: PropTypes.object.isRequired, // Tambah servicesManager prop
};
export { StudyItem };