Skip to content

Commit

Permalink
Merge pull request #2 from SteveGT96/feature/OH2-422-studies-series
Browse files Browse the repository at this point in the history
OH2-422 | OH2-423 | Implement study series and instance preview
  • Loading branch information
SilverD3 authored Dec 12, 2024
2 parents f45a3e2 + ce09808 commit 6f0861d
Show file tree
Hide file tree
Showing 25 changed files with 643 additions and 66 deletions.
18 changes: 12 additions & 6 deletions api/oh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3897,9 +3897,9 @@ paths:
"200":
description: OK
content:
application/octet-stream:
application/json:
schema:
type: string
$ref: "#/components/schemas/InstancePreviewDTO"
security:
- bearerAuth: []
/pricelists/prices:
Expand Down Expand Up @@ -6321,13 +6321,13 @@ components:
description: lock
format: int32
example: 0
pharmacy:
type: boolean
male:
opd:
type: boolean
female:
type: boolean
opd:
male:
type: boolean
pharmacy:
type: boolean
PatientDTO:
required:
Expand Down Expand Up @@ -8425,6 +8425,12 @@ components:
type: string
stable:
type: boolean
InstancePreviewDTO:
type: object
properties:
data:
type: string
description: Base64 representation of the instance preview
PriceDTO:
required:
- description
Expand Down
1 change: 1 addition & 0 deletions src/components/accessories/radiology/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useViewInOrthanc";
17 changes: 17 additions & 0 deletions src/components/accessories/radiology/hooks/useViewInOrthanc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useCallback } from "react";

export const useViewInOrthanc = (level: "study" | "series" | "instance") => {
/**
* Todo: Use value provided by the backend
*/
const ORTHANC_EXPLORER =
"https://orthanc.uni2growcameroun.com/app/explorer.html";
const handleViewInOrthanc = useCallback(
(row: any) => () => {
window.open(`${ORTHANC_EXPLORER}#${level}?uuid=${row.id}`, "_blank");
},
[level]
);

return handleViewInOrthanc;
};
1 change: 1 addition & 0 deletions src/components/accessories/radiology/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./Radiology";
export * from "./series/Series";
export * from "./studies/Studies";
247 changes: 247 additions & 0 deletions src/components/accessories/radiology/series/Series.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { ChevronLeft, OpenInNew } from "@mui/icons-material";
import {
Backdrop,
Button,
CircularProgress,
IconButton,
Tooltip,
} from "@mui/material";
import InfoBox from "components/accessories/infoBox/InfoBox";
import Table from "components/accessories/table/Table";
import { TFilterField } from "components/accessories/table/filter/types";
import { useAppDispatch, useAppSelector } from "libraries/hooks/redux";
import { isEmpty } from "lodash";
import moment from "moment";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate, useParams } from "react-router";
import {
SeriesWithInstances,
getInstancePreview,
getInstancePreviewReset,
getStudySeriesReset,
getStudySeriesWithInstances,
getStudySeriesWithInstancesReset,
} from "state/radiology";
import { useViewInOrthanc } from "../hooks";
import { Instances } from "./instances/Instances";
import { Preview } from "./preview/Preview";
import "./styles.scss";

export const Series = () => {
const { t, i18n } = useTranslation();
const dispatch = useAppDispatch();

const navigate = useNavigate();

const { id } = useParams();

const { state: study } = useLocation();

const [openPreview, setOpenPreview] = useState(false);

const seriesState = useAppSelector(
(state) => state.radiology.seriesWithInstances
);

const previewState = useAppSelector((state) => state.radiology.preview);

const header = ["title", "lastUpdate", "instances"];
const dateFields = ["lastUpdate"];

const label = {
title: t("radiology.series.title"),
status: t("radiology.series.status"),
operators: t("radiology.series.operators"),
instances: t("radiology.series.instances"),
station: t("radiology.series.station"),
expectedInstances: t("radiology.series.expectedInstances"),
lastUpdate: t("radiology.series.lastUpdate"),
};
const order = ["lastUdate", "instances"];

const filters: TFilterField[] = [
{
key: "title",
label: t("radiology.series.title"),
type: "text",
},
{
key: "lastUpdate",
label: t("radiology.series.lastUpdate"),
type: "date",
},
{ key: "instances", label: t("radiology.series.intances"), type: "number" },
];

useEffect(() => {
if (id) {
dispatch(getStudySeriesWithInstances(id));
}
}, [dispatch, id]);

useEffect(() => {
return () => {
dispatch(getStudySeriesWithInstancesReset());
};
}, [dispatch]);

const formatDataToDisplay = (data: SeriesWithInstances[]) => {
return data.map((series) => {
return {
id: series.id ?? "",
title: isEmpty(series.series?.seriesDescription)
? "--"
: series.series?.seriesDescription,
instancesData: series.instances,
instances: series.instancesIds?.length ?? 0,
expectedInstances: series.expectedNumberOfInstances ?? "",
lastUpdate: series.lastUpdate
? moment(series.lastUpdate).locale(i18n.language).format("L")
: "",
operators: series.series?.operatorsName ?? "",
protocol: series.series?.protocolName ?? "",
station: series.series?.stationName ?? "",
status: series.status ?? "",
};
});
};

const navigateToStudies = useCallback(() => {
navigate("..");
}, [navigate]);

useEffect(() => {
return () => {
getStudySeriesReset();
if (previewState.status !== "IDLE") {
getInstancePreviewReset();
}
};
}, [dispatch]);

const handlePreview = useCallback(
(row: any) => () => {
dispatch(getInstancePreview(row.id));
},
[dispatch]
);

const handleClosePreview = useCallback(() => {
setOpenPreview(false);
dispatch(getInstancePreviewReset());
}, [dispatch, setOpenPreview]);

useEffect(() => {
if (previewState.hasSucceeded) {
setOpenPreview(true);
}
}, [previewState.status, setOpenPreview]);

const handleViewSeries = useViewInOrthanc("series");

return (
<div className="series">
{(() => {
switch (seriesState.status) {
case "FAIL":
return (
<InfoBox
type="error"
message={t(
seriesState.error?.message ?? "common.somethingwrong"
)}
/>
);
case "LOADING":
return (
<CircularProgress
style={{ marginLeft: "50%", position: "relative" }}
/>
);

case "SUCCESS":
return (
<>
<Button
variant="text"
color="secondary"
onClick={navigateToStudies}
>
<ChevronLeft /> {t("radiology.series.backToStudies")}
</Button>
{study?.title && (
<p className="series__studyTitle">
{study.title} {study.date && " | " + study.date}
</p>
)}
{previewState.hasFailed && (
<InfoBox
type="error"
message={t(
previewState.error?.message ?? "common.somethingwrong"
)}
/>
)}
<Table
rowData={formatDataToDisplay(seriesState.data ?? [])}
dateFields={dateFields}
tableHeader={header}
labelData={label}
columnsOrder={order}
rowsPerPage={5}
isCollapsabile={true}
renderCustomActions={(row) => (
<div className="series__actions">
<Tooltip title={t("radiology.series.viewInOrthanc")}>
<IconButton onClick={handleViewSeries(row)}>
<OpenInNew />
</IconButton>
</Tooltip>
</div>
)}
filterColumns={filters}
rawData={(seriesState.data ?? []).map((series) => ({
id: series.id ?? "",
title: series.series?.seriesDescription ?? "",
lastUpdate: series.lastUpdate
? moment(series.lastUpdate)?.toISOString()
: "",
}))}
manualFilter={false}
rowKey="id"
customRenderDetails={(row) => (
<Instances
onPreview={handlePreview}
data={row.instancesData}
/>
)}
/>

<Backdrop
sx={{
color: "#fff",
zIndex: (theme) => theme.zIndex.drawer + 1,
}}
open={previewState.isLoading}
>
<CircularProgress color="inherit" />
</Backdrop>
<Preview open={openPreview} onClose={handleClosePreview} />
</>
);

case "SUCCESS_EMPTY":
return (
<>
<InfoBox type="info" message={t("common.emptydata")} />
</>
);

default:
return;
}
})()}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Visibility } from "@mui/icons-material";
import { IconButton, Tooltip } from "@mui/material";
import Table from "components/accessories/table/Table";
import { InstanceResponse } from "generated";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import "./styles.scss";

interface IOwnProps {
data: InstanceResponse[];
onPreview: (row: any) => () => void;
}

export const Instances = ({ data, onPreview }: IOwnProps) => {
const { t, i18n } = useTranslation();

const header = ["title", "date", "time"];

const label = {
title: t("radiology.instances.title"),
date: t("radiology.instances.date"),
time: t("radiology.instances.time"),
};

const order = ["title", "date", "time"];

const formatDataToDisplay = (data: InstanceResponse[]) => {
return data.map((instance) => {
return {
id: instance.id ?? "",
title: t("radiology.instances.title", {
number: instance.instance?.instanceNumber ?? "",
}),
date: instance.instance?.creationDate
? moment(instance.instance.creationDate, "YYYYMMDD")
.locale(i18n.language)
.format("L")
: "",
time: instance.instance?.creationTime
? moment(instance.instance?.creationTime, "HHmmss")
.locale(i18n.language)
.format("LTS")
: "",
fileUid: instance.fileUuid ?? "",
};
});
};

return (
<div className="instances">
<Table
columnsOrder={order}
rowData={formatDataToDisplay(data)}
tableHeader={header}
labelData={label}
rowsPerPage={data.length}
isCollapsabile={false}
renderCustomActions={(row) => (
<div className="instances__actions">
<Tooltip title={t("radiology.instances.preview")}>
<IconButton onClick={onPreview(row)}>
<Visibility />
</IconButton>
</Tooltip>
</div>
)}
hideHeader={true}
hidePaginator={true}
/>
</div>
);
};
Loading

0 comments on commit 6f0861d

Please sign in to comment.