diff --git a/src/admin/general/OrthancCard.tsx b/src/admin/general/OrthancCard.tsx index a9a84fe0..70a27e9d 100644 --- a/src/admin/general/OrthancCard.tsx +++ b/src/admin/general/OrthancCard.tsx @@ -172,7 +172,9 @@ const OrthancSettingsCard = ({ orthancData }: OrthancCardProps) => { )} - setShowModal(false)}> + setShowModal(false)}> Close diff --git a/src/anonymize/AnonQueues.tsx b/src/anonymize/AnonQueues.tsx new file mode 100644 index 00000000..093197ba --- /dev/null +++ b/src/anonymize/AnonQueues.tsx @@ -0,0 +1,47 @@ +import { useSelector } from "react-redux"; +import { deleteAnonymizeQueue, getAnonymizeQueue, getExistingAnonymizeQueues } from "../services/queues"; +import { useCustomMutation, useCustomQuery } from "../utils"; +import { RootState } from "../store"; +import { Spinner } from "../ui"; +import ProgressQueueBar from "../queue/ProgressQueueBar"; +import { Queue } from "../utils/types"; + +const AnonQueues = () => { + const currentUserId = useSelector((state: RootState) => state.user.currentUserId); + + const { data: existingAnonymizeQueues } = useCustomQuery( + ['queue', 'anon', currentUserId?.toString() || ''], + () => getExistingAnonymizeQueues(currentUserId) + ); + + const firstQueue = existingAnonymizeQueues?.[0] + + const { data, isPending , isLoading} = useCustomQuery( + ['queue', 'anon', firstQueue], + () => getAnonymizeQueue(firstQueue), + { refetchInterval: 2000, + enabled : existingAnonymizeQueues?.length > 0 } + ); + + const { mutate: mutateDeleteQueue } = useCustomMutation( + () => deleteAnonymizeQueue(firstQueue), + [['queue', 'anon']] + ); + + if (existingAnonymizeQueues?.length > 0 && isPending) return ; + + return ( + + {existingAnonymizeQueues?.map((uuid) => ( + + + + ))} + + ); +}; + +export default AnonQueues; diff --git a/src/anonymize/AnonymizeRoot.tsx b/src/anonymize/AnonymizeRoot.tsx index 60c2a8d8..3ffceb35 100644 --- a/src/anonymize/AnonymizeRoot.tsx +++ b/src/anonymize/AnonymizeRoot.tsx @@ -1,86 +1,200 @@ import { useDispatch, useSelector } from "react-redux"; -import { Card, CardHeader, CardBody, CardFooter, Button } from "../ui"; -import { Colors } from "../utils"; +import { Card, CardHeader, CardBody, CardFooter, Button, SelectInput, Input, CheckBox } from "../ui"; +import { Colors, useCustomMutation } from "../utils"; import PatientTable from "./PatientTable"; import StudyTable from "./StudyTable"; import { RootState } from "../store"; import { useMemo, useState } from "react"; -import { flushAnonymizeList, removeStudyFromAnonymizeList, updateAnonymizePatientValue, updateAnonymizeStudyValue } from "../reducers/AnonymizeSlice"; -import { Empty } from "../icons"; -import { Patient } from "../utils/types"; +import { + flushAnonymizeList, + removeStudyFromAnonymizeList, + updateAnonymizationProfile, + updateAnonymizePatientValue, + updateAnonymizeStudyValue, +} from "../reducers/AnonymizeSlice"; +import { Anon, Empty } from "../icons"; +import AutoFill from "../icons/AutofIll"; +import { AnonItem } from "../utils/types"; +import { createAnonymizeQueue } from "../services/queues"; +import AnonQueues from "./AnonQueues"; +import DropdownButton from "../ui/menu/DropDownButton"; + +const profileOptions = [ + { value: "Default", label: "Default" }, + { value: "Full", label: "Full" }, +]; const AnonymizeRoot = () => { const dispatch = useDispatch(); const anonList = useSelector((state: RootState) => state.anonymize); - const [selectedPatientId, setSelectedPatientId] = useState(null) + const [selectedPatientId, setSelectedPatientId] = useState( + null + ); - const patients = useMemo(() => { - return Object.values(anonList.patients); - }, [anonList]); + const [anonJobId, setAnonJobId] = useState(null); + + const { mutate: mutateCreateAnonymizeQueue } = useCustomMutation( + ({ anonItems }) => createAnonymizeQueue(anonItems), + [['queue', 'anon']], + { + onSuccess: (jobId) => { + setAnonJobId(jobId) + }, + } + ) + + const patients = useMemo(() => Object.values(anonList.patients), [anonList]); const studies = useMemo(() => { - if (!selectedPatientId) return [] - return Object.values(anonList.studies).filter(study => study.originalStudy.parentPatient === selectedPatientId); + if (!selectedPatientId) return []; + return Object.values(anonList.studies).filter( + (study) => study.originalStudy.parentPatient === selectedPatientId + ); }, [anonList, selectedPatientId]); - const handlePatientSelect = (patient: Patient) => { - setSelectedPatientId(patient.id) - } - const handleRemovePatient = (patientId: string) => { - const studiesIds = studies - .filter((study) => study.originalStudy.parentPatient === patientId) - .map((study) => study.originalStudy.id); - for (const studyId of studiesIds) { - dispatch(removeStudyFromAnonymizeList({ studyId })); - } - }; + const handleAutoFill = () => { + patients.forEach((patient) => { + dispatch( + updateAnonymizePatientValue({ + patientId: patient.originalPatient.id, + newPatientName: `Patient_${patient.originalPatient.id}`, + newPatientId: `ID_${patient.originalPatient.id}`, + }) + ); + }); - const handleRemoveStudy = (studyId: string) => { - dispatch(removeStudyFromAnonymizeList({ studyId })); + studies.forEach((study) => { + dispatch( + updateAnonymizeStudyValue({ + studyId: study.originalStudy.id, + newStudyDescription: `Study_${study.originalStudy.id}`, + newAccessionNumber: `Acc_${study.originalStudy.id}`, + }) + ); + }); }; - const handleChangeStudy = (studyId: string, key: string, newStudyDescription: string) => { - console.log(studyId, key, newStudyDescription) - dispatch(updateAnonymizeStudyValue({ newStudyDescription, studyId })); - }; + const onChangeStudy = (studyId, key, value) => { + dispatch( + updateAnonymizeStudyValue({ studyId, [key]: value }) + ) + } - const handleChangePatient = (patientId: string, key: string, value: string) => { - dispatch(updateAnonymizePatientValue({ patientId, [key]: value })); + const onRemoveStudy = (studyId) => { + dispatch(removeStudyFromAnonymizeList({ studyId })) } - const handleClearList = () => { - dispatch(flushAnonymizeList()); - }; + const onChangePatient = (patientId, key, value) => { + dispatch( + updateAnonymizePatientValue({ patientId, [key]: value }) + ) + } + + const onRemovePatient = (patientId) => { + studies.filter((study) => study.originalStudy.parentPatient === patientId). + forEach((study) => { + console.log(study.originalStudy.id) + dispatch( + removeStudyFromAnonymizeList({ studyId: study.originalStudy.id }) + ) + } + ) + + } + + const onChangeProfile = (option) => { + dispatch(updateAnonymizationProfile({ anonymizationProfile: option.value })) + } + + const handleAnonymizeStart = () => { + const anonItems: AnonItem[] = Object.values(anonList.studies).map((study) => { + return { + OrthancStudyID: study.originalStudy.id, + Profile: anonList.anonymizationProfile, + NewPatientID: study.newPatientId, + NewPatientName: study.newPatientName, + NewStudyDescription: study.newStudyDescription, + NewAccessionNumber: study.newAccessionNumber + } + + }) + mutateCreateAnonymizeQueue({ anonItems }) + } return ( - + - Anonymize Ressources - + + Anonymize resources + + + + + + + + + + dispatch(flushAnonymizeList())} + color={Colors.light} + className="rounded-lg hover:bg-secondary group" + > + + - + className="rounded-lg hover:bg-secondary group" + > + - - + + - + - + + + + Anonymise + + + ); diff --git a/src/anonymize/PatientTable.tsx b/src/anonymize/PatientTable.tsx index 8a4cd809..75907cb0 100644 --- a/src/anonymize/PatientTable.tsx +++ b/src/anonymize/PatientTable.tsx @@ -1,53 +1,89 @@ +import React, { useMemo } from "react"; import { ColumnDef } from "@tanstack/react-table"; -import { Button, Input, Table } from "../ui" -import { useMemo } from "react"; -import { Patient } from "../utils/types"; +import { Button, Table } from "../ui"; +import { AnonPatient } from "../utils/types"; import { Colors } from "../utils"; import { Trash } from "../icons"; type PatientTableProps = { - patients: Patient[] - onClickRow: (patient: Patient) => void; - onRemovePatient: (patientId: string) => void - onCellEdit: (patientId: string | number, columnId: any, value: any) => void -} -const PatientTable = ({ patients, onClickRow, onRemovePatient, onCellEdit }: PatientTableProps) => { + patients: AnonPatient[]; + selectedRows?: Record; + onClickRow: (patientId :string) => void; + onRemovePatient: (patientId: string) => void; + onChangePatient: (patientId: string | number, columnId: any, value: any) => void; + onRowSelectionChange?: (selectedRow: Record) => void; +}; - - const columns: ColumnDef[] = useMemo(() => [ +const PatientTable = ({ + patients, + selectedRows, + onClickRow, + onRemovePatient, + onChangePatient, + onRowSelectionChange, +}: PatientTableProps) => { + console.log(patients) + const columns: ColumnDef[] = useMemo(() => [ { id: "id", accessorKey: "id" }, { - accessorKey: "mainDicomTags.patientId", + accessorKey: "originalPatient.mainDicomTags.patientId", header: "Patient ID", }, { - accessorKey: "mainDicomTags.patientName", + accessorKey: "originalPatient.mainDicomTags.patientName", header: "Patient Name", }, { id: "newPatientId", + accessorKey: "newPatientId", header: "New Patient ID", isEditable: true }, { id: "newPatientName", + accessorKey: "newPatientName", header: "New Patient Name", isEditable: true }, { header: "remove", cell: ({ row }) => { - return onRemovePatient(row.original.id)}>; + return ( + onRemovePatient(row.original.originalPatient.id)}> + + + ); }, }, - ], []); + ], [onRemovePatient]); + + const getRowClasses = (row: AnonPatient) => { + if (selectedRows?.[row.originalPatient.id]) { + return 'bg-primary hover:cursor-pointer'; + } else { + return 'hover:bg-indigo-100 hover:cursor-pointer'; + } + }; return ( - - ) -} + onClickRow(row.originalPatient.id)} + onCellEdit={onChangePatient} + getRowClasses={getRowClasses} + selectedRow={selectedRows} + onRowSelectionChange={onRowSelectionChange} + getRowId={(row) => row.originalPatient.id} + /> + ); +}; -export default PatientTable \ No newline at end of file +export default PatientTable; diff --git a/src/anonymize/StudyTable.tsx b/src/anonymize/StudyTable.tsx index e73331c9..3a0f085e 100644 --- a/src/anonymize/StudyTable.tsx +++ b/src/anonymize/StudyTable.tsx @@ -1,21 +1,20 @@ -import { Button, Table } from "../ui"; +import { Button, Input, Table } from "../ui"; import { Colors } from "../utils"; import { Trash } from "../icons"; import { ColumnDef } from "@tanstack/react-table"; import { AnonStudy } from "../utils/types"; -import { useMemo } from "react"; type StudyTableProps = { studies: AnonStudy[]; + onChangeStudy: (studyId: string, key:string, value: string) => void; onRemoveStudy: (studyId: string) => void; - onCellEdit: (studyId: string | number, columnId: any, value: any) => void }; -const StudyTable = ({ studies, onRemoveStudy, onCellEdit }: StudyTableProps) => { - const columns: ColumnDef[] = useMemo(() => [ +const StudyTable = ({ studies, onChangeStudy, onRemoveStudy }: StudyTableProps) => { + const columns: ColumnDef[] = [ { id: "id", - accessorFn: (row)=> row.originalStudy.id, + accessorKey: "originalStudy.id", }, { accessorKey: "originalStudy.mainDicomTags.studyDate", @@ -26,9 +25,10 @@ const StudyTable = ({ studies, onRemoveStudy, onCellEdit }: StudyTableProps) => header: "Study Description", }, { - id: "newStudyDescription", + id : "newStudyDescription", + accessorKey: "newStudyDescription", header: "New Study Description", - isEditable: true + isEditable: true, }, { header: "Remove", @@ -45,9 +45,19 @@ const StudyTable = ({ studies, onRemoveStudy, onCellEdit }: StudyTableProps) => ); }, }, - ], []); + ]; + + return row.originalStudy.id} + + />; - return ; }; export default StudyTable; diff --git a/src/assets/Anon.svg b/src/assets/Anon.svg deleted file mode 100644 index 61294a7f..00000000 --- a/src/assets/Anon.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - \ No newline at end of file diff --git a/src/content/ContentRoot.tsx b/src/content/ContentRoot.tsx index dda67149..f457b86c 100644 --- a/src/content/ContentRoot.tsx +++ b/src/content/ContentRoot.tsx @@ -25,8 +25,7 @@ import { addStudyIdToAnonymizeList, } from "../utils/actionsUtils"; import { Colors } from "../utils"; -import AnonIcon from "../assets/Anon.svg?react"; -import { Export } from "../icons"; +import { Anon, Export } from "../icons"; import Labels from "./Labels"; @@ -173,7 +172,7 @@ const ContentRoot: React.FC = () => { className="flex items-center text-sm transition-transform duration-200 bg-blue-700 hover:scale-105" onClick={handleSendAnonymizeList} > - + Send to Anonymize diff --git a/src/delete/DeleteQueues.tsx b/src/delete/DeleteQueues.tsx index 982515b3..d35ac789 100644 --- a/src/delete/DeleteQueues.tsx +++ b/src/delete/DeleteQueues.tsx @@ -1,30 +1,59 @@ import { useSelector } from "react-redux"; -import { getExistingDeleteQueues } from "../services/queues"; -import { useCustomQuery } from "../utils"; +import { deleteDeleteQueue, getDeleteQueue, getExistingDeleteQueues } from "../services/queues"; +import { useCustomMutation, useCustomQuery } from "../utils"; import { RootState } from "../store"; -import { Spinner } from "../ui"; +import { ProgressCircle, Spinner } from "../ui"; import ProgressQueueBar from "../queue/ProgressQueueBar"; +import { Queue } from "../utils/types"; +import ProgressQueueCircle from "../queue/ProgressQueueCircle"; -const DeleteQueues = () => { +type DeleteQueueProps = { + circle?: boolean +} + +const DeleteQueues = ({ circle = false }: DeleteQueueProps) => { const currentUserId = useSelector((state: RootState) => state.user.currentUserId); - const { data: existingDeleteQueues, isPending } = useCustomQuery( + const { data: existingDeleteQueues } = useCustomQuery( ['queue', 'delete', currentUserId?.toString() || ''], () => getExistingDeleteQueues(currentUserId) ); + const firstQueue = existingDeleteQueues?.[0] + + const { data, isPending } = useCustomQuery( + ['queue', 'delete', firstQueue], + () => getDeleteQueue(firstQueue), + { + refetchInterval: 2000, + enabled: existingDeleteQueues?.length > 0 + } + ); + + const { mutate: mutateDeleteQueue } = useCustomMutation( + () => deleteDeleteQueue(firstQueue), + [['queue', 'delete']] + ); + if (isPending) return ; return ( - - {existingDeleteQueues?.map((uuid) => ( - - - - ))} - + + {existingDeleteQueues?.map((uuid) => ( + + { + circle ? + mutateDeleteQueue({})} /> + : + mutateDeleteQueue({})} /> + } + + + ))} + ); }; diff --git a/src/delete/DeleteRoot.tsx b/src/delete/DeleteRoot.tsx index 81988239..60878b6a 100644 --- a/src/delete/DeleteRoot.tsx +++ b/src/delete/DeleteRoot.tsx @@ -55,24 +55,27 @@ const DeleteRoot = () => { - + - - - - Delete List - + + + + + Delete List + - - + + + + + ); }; diff --git a/src/delete/DeleteStudyTable.tsx b/src/delete/DeleteStudyTable.tsx index d0d22c27..655927ad 100644 --- a/src/delete/DeleteStudyTable.tsx +++ b/src/delete/DeleteStudyTable.tsx @@ -52,7 +52,7 @@ const DeleteStudyTable = ({ studies }: DeleteStudyTableProps) => { handleDelete(row.original.id)} - color={Colors.warning} + color={Colors.danger} > diff --git a/src/export/ExportRoot.tsx b/src/export/ExportRoot.tsx index ed57825b..779cf874 100644 --- a/src/export/ExportRoot.tsx +++ b/src/export/ExportRoot.tsx @@ -176,7 +176,10 @@ const ExportRoot = () => { - setTrasferSyntax(value)} /> + setTrasferSyntax(value)} + /> @@ -188,20 +191,30 @@ const ExportRoot = () => { - - + - - + + {storeJobId && } - + {sendPeerJobId && } - + Send to diff --git a/src/export/ExportStudyTable.tsx b/src/export/ExportStudyTable.tsx index 47800299..4582ef6e 100644 --- a/src/export/ExportStudyTable.tsx +++ b/src/export/ExportStudyTable.tsx @@ -9,7 +9,7 @@ import { Trash } from "../icons"; type ExportStudyTableProps = { studies: Study[]; - onClickStudy : (study : Study) => void + onClickStudy: (study: Study) => void }; const ExportStudyTable = ({ studies, onClickStudy }: ExportStudyTableProps) => { @@ -17,8 +17,8 @@ const ExportStudyTable = ({ studies, onClickStudy }: ExportStudyTableProps) => { const handleDelete = (studyId: string) => { const studyToDelete = studies.find(study => study.id === studyId) - for(const seriesId of studyToDelete.series){ - dispatch(removeSeriesFromExportList({seriesId : seriesId})) + for (const seriesId of studyToDelete.series) { + dispatch(removeSeriesFromExportList({ seriesId: seriesId })) } }; @@ -65,7 +65,12 @@ const ExportStudyTable = ({ studies, onClickStudy }: ExportStudyTableProps) => { [] ); - return ; + return ; }; export default ExportStudyTable; diff --git a/src/export/SelectTransferSyntax.tsx b/src/export/SelectTransferSyntax.tsx index 2cb4534b..763ca412 100644 --- a/src/export/SelectTransferSyntax.tsx +++ b/src/export/SelectTransferSyntax.tsx @@ -1,12 +1,11 @@ +import { useState, useEffect } from "react"; import { Gear } from "../icons"; -import { Button, Popover, SelectInput } from "../ui"; -import { Colors } from "../utils"; +import { Popover, SelectInput } from "../ui"; type SelectTransferSyntaxProps = { value: string; - onChange: (value: string) => void -} - + onChange: (value: string) => void; +}; const TRANSCODING_OPTIONS = [ { value: 'None', label: 'None (use Original TS)' }, @@ -23,24 +22,75 @@ const TRANSCODING_OPTIONS = [ { value: '1.2.840.10008.1.2.4.90', label: 'JPEG 2000 (90)' }, { value: '1.2.840.10008.1.2.4.91', label: 'JPEG 2000 (91)' }, { value: '1.2.840.10008.1.2.4.92', label: 'JPEG 2000 (92)' }, - { value: '1.2.840.10008.1.2.4.93', label: 'JPEG 2000 (93)' } -] + { value: '1.2.840.10008.1.2.4.93', label: 'JPEG 2000 (93)' }, +]; const SelectTransferSyntax = ({ value, onChange }: SelectTransferSyntaxProps) => { + const [clicked, setClicked] = useState(false); + const [hovered, setHovered] = useState(false); + + const handleClick = () => { + setClicked(prev => !prev); + setHovered(false); + }; + + const handleMouseEnter = () => { + if (!clicked) { + setHovered(true); + } + }; + + const handleMouseLeave = () => { + setHovered(false); + }; + + useEffect(() => { + const handleOutsideClick = (event: MouseEvent) => { + if (!event.target.closest('.popover-container')) { + setClicked(false); + setHovered(false); + } + }; + + if (clicked) { + document.addEventListener('mousedown', handleOutsideClick); + } + + return () => { + document.removeEventListener('mousedown', handleOutsideClick); + }; + }, [clicked]); return ( onChange(option?.value)} />} + { + onChange(option?.value); + setClicked(false); + }} + /> + } + backgroundColor="bg-secondary" > - - + + - ) - -} + ); +}; -export default SelectTransferSyntax \ No newline at end of file +export default SelectTransferSyntax; \ No newline at end of file diff --git a/src/ui/AnonIcon.tsx b/src/icons/Anon.tsx similarity index 85% rename from src/ui/AnonIcon.tsx rename to src/icons/Anon.tsx index 378f52fa..08ab01ed 100644 --- a/src/ui/AnonIcon.tsx +++ b/src/icons/Anon.tsx @@ -1,12 +1,14 @@ -// AnonIcon.tsx -const AnonIcon = ({ className }) => ( +type AnonProps = { + className?: string; +} +const Anon = ({ className = "" } : AnonProps) => ( ( ); -export default AnonIcon; +export default Anon; diff --git a/src/icons/AutofIll.tsx b/src/icons/AutofIll.tsx new file mode 100644 index 00000000..d3a94753 --- /dev/null +++ b/src/icons/AutofIll.tsx @@ -0,0 +1,9 @@ +import { MdAutoFixHigh } from 'react-icons/md'; + +const AutoFill = (props) => { + return ( + + ); +}; + +export default AutoFill; diff --git a/src/icons/index.ts b/src/icons/index.ts index 1c6d5324..e2da62e5 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -1,5 +1,6 @@ import Add from './Add' import Admin from './Admin' +import Anon from './Anon' import ArrowLeft from './ArrowLeft' import Brain from './Brain' import Cancel from './Cancel' @@ -53,6 +54,7 @@ import ZoomQuestion from './ZoomQuestion' export { Add, Admin, + Anon, ArrowLeft, Brain, Cancel, diff --git a/src/import/import/ImportRoot.tsx b/src/import/import/ImportRoot.tsx index 360e9d27..4993a9a2 100644 --- a/src/import/import/ImportRoot.tsx +++ b/src/import/import/ImportRoot.tsx @@ -70,7 +70,7 @@ const ImportRoot: React.FC = () => { {errors.length > 0 && ( void }; -const ProgressQueueBar= ({ uuid }: ProgressQueueProps) => { - const { data, isPending } = useCustomQuery( - ['queue', 'delete', uuid], - () => getDeleteQueue(uuid), - { refetchInterval: 2000 } - ); - - const { mutate: mutateDeleteQueue } = useCustomMutation( - () => deleteDeleteQueue(uuid), - [['queue', 'delete']] - ); - - if (isPending) return ; +const ProgressQueueBar = ({ queueData, onDelete }: ProgressQueueProps) => { return ( - - - {data?.state} - - {/* Implement pause functionality here */ }} - /> + + + mutateDeleteQueue({})} + className="text-sm cursor-pointer text-danger hover:text-danger-hover" + onClick={onDelete} /> diff --git a/src/queue/ProgressQueueCircle.tsx b/src/queue/ProgressQueueCircle.tsx index c8b5e61d..676488ec 100644 --- a/src/queue/ProgressQueueCircle.tsx +++ b/src/queue/ProgressQueueCircle.tsx @@ -1,39 +1,22 @@ -import { deleteDeleteQueue, getDeleteQueue } from "../services/queues"; -import { useCustomMutation, useCustomQuery } from "../utils"; -import { ProgressCircle, Spinner } from "../ui"; -import { Cancel, Pause } from "../icons"; +import { ProgressCircle } from "../ui"; +import { Cancel } from "../icons"; +import { Queue } from "../utils/types"; type ProgressQueueProps = { - uuid: string; + queueData: Queue, + onDelete: (event: React.MouseEvent) => void }; -const ProgressQueueCircle = ({ uuid }: ProgressQueueProps) => { - const { data, isPending } = useCustomQuery( - ['queue', 'delete', uuid], - () => getDeleteQueue(uuid), - { refetchInterval: 2000 } - ); - - const { mutate: mutateDeleteQueue } = useCustomMutation( - () => deleteDeleteQueue(uuid), - [['queue', 'delete']] - ); - - if (isPending) return ; +const ProgressQueueCircle = ({ queueData, onDelete }: ProgressQueueProps) => { return ( - + - {/* Implement pause functionality here */ }} - /> mutateDeleteQueue({})} - /> + className={`text-sm text-danger cursor-pointer hover:text-danger-hover `} + onClick={onDelete} + /> diff --git a/src/reducers/AnonymizeSlice.ts b/src/reducers/AnonymizeSlice.ts index 6e7d62f4..0cd4577e 100644 --- a/src/reducers/AnonymizeSlice.ts +++ b/src/reducers/AnonymizeSlice.ts @@ -1,9 +1,9 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { AnonStudy, Patient } from "../utils/types"; +import { AnonPatient, AnonStudy } from "../utils/types"; export type AnonymizeState = { patients: { - [patientId: string]: Patient; + [patientId: string]: AnonPatient; }; studies: { [studyId: string]: AnonStudy; @@ -16,7 +16,7 @@ type setAnonymizationProfilePayload = { }; type AddAnonymizePayload = { - patient: Patient; + patient: AnonPatient; study: AnonStudy; }; @@ -46,7 +46,7 @@ const anonymizeSlice = createSlice({ name: "anonymize", initialState, reducers: { - setAnonymizationProfile: ( + updateAnonymizationProfile: ( state, action: PayloadAction ) => { @@ -56,9 +56,9 @@ const anonymizeSlice = createSlice({ state, action: PayloadAction ) => { - const study = action.payload.study; - const patientId = action.payload.patient.id; - state.studies[study.originalStudy.id] = study; + const studyId = action.payload.study.originalStudy.id; + const patientId = action.payload.patient.originalPatient.id; + state.studies[studyId] = action.payload.study; state.patients[patientId] = action.payload.patient; }, updateAnonymizePatientValue: ( @@ -67,15 +67,21 @@ const anonymizeSlice = createSlice({ ) => { const patientId = action.payload.patientId; + const studyIdsToUpdate = Object.values(state.studies) .filter((study) => study.originalStudy.parentPatient === patientId) .map((study) => study.originalStudy.id); for (const studyId of studyIdsToUpdate) { - if (action.payload.newPatientName) + if (action.payload.newPatientName) { state.studies[studyId].newPatientName = action.payload.newPatientName; - if (action.payload.newPatientId) + state.patients[patientId].newPatientName = action.payload.newPatientName; + } + + if (action.payload.newPatientId) { state.studies[studyId].newPatientId = action.payload.newPatientId; + state.patients[patientId].newPatientId = action.payload.newPatientId; + } } }, @@ -114,7 +120,7 @@ const anonymizeSlice = createSlice({ }); export const { - setAnonymizationProfile, + updateAnonymizationProfile, addStudyToAnonymizeList, removeStudyFromAnonymizeList, flushAnonymizeList, diff --git a/src/root/ToolItem.tsx b/src/root/ToolItem.tsx index 1bcb7e2b..38ff964d 100644 --- a/src/root/ToolItem.tsx +++ b/src/root/ToolItem.tsx @@ -6,7 +6,7 @@ type ToolItemProps = { const ToolItem = ({ children, count, onClick }: ToolItemProps) => { return ( - + {children} {count} diff --git a/src/root/ToolList.tsx b/src/root/ToolList.tsx index e8a4f832..5606669c 100644 --- a/src/root/ToolList.tsx +++ b/src/root/ToolList.tsx @@ -1,10 +1,9 @@ import { useSelector } from "react-redux" import { RootState } from "../store" -import AnonIcon from "../ui/AnonIcon" import ToolItem from "./ToolItem" import { useNavigate } from "react-router-dom" -import { Export, Trash } from "../icons" +import { Anon, Export, Trash } from "../icons" const ToolList = () => { @@ -16,10 +15,10 @@ const ToolList = () => { return ( - + navigate('/anonymize')}> - navigate('/export')}> diff --git a/src/services/queues.ts b/src/services/queues.ts index 1fbd007c..c2fe226a 100644 --- a/src/services/queues.ts +++ b/src/services/queues.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import { Queue } from "../utils/types"; +import { AnonItem, AnonymizePayload, Queue } from "../utils/types"; export const createDeleteQueue = (seriesId: string[]): Promise => { const payload = { @@ -63,4 +63,70 @@ export const getExistingDeleteQueues = (userId: number | undefined): Promise => { + const payload : AnonymizePayload = { + Anonymizes: anonItems + } + return axios + .post(`/api/queues/anonymize`, payload) + .then((response) => response.data.Uuid) + .catch(function (error) { + if (error.response) { + throw error.response; + } + throw error; + }); +}; + +export const getExistingAnonymizeQueues = (userId: number | undefined): Promise => { + const url = userId ? `/api/queues/anonymize?userId=${userId}` : '/api/queues/anon' + return axios + .get(url) + .then((response) => response.data) + .catch(function (error) { + if (error.response) { + throw error.response; + } + throw error; + }); +}; + + +export const getAnonymizeQueue = (uuid: string): Promise => { + + return axios + .get(`/api/queues/anonymize/${uuid}`) + .then((response) => { + const data: any = Object.values(response.data)[0]; + return { + userId: data.UserId, + progress: data.Progress, + state: data.State, + id: data.Id, + results: data.Results + } + }) + .catch(function (error) { + if (error.response) { + throw error.response; + } + throw error; + }); +}; + + +export const deleteAnonymizeQueue = (uuid: string): Promise => { + + return axios + .delete(`/api/queues/anonymize/${uuid}`) + .then(() => undefined) + .catch(function (error) { + if (error.response) { + throw error.response; + } + throw error; + }); }; \ No newline at end of file diff --git a/src/ui/BannerAlert.tsx b/src/ui/BannerAlert.tsx index be476e37..ad34e98d 100644 --- a/src/ui/BannerAlert.tsx +++ b/src/ui/BannerAlert.tsx @@ -12,7 +12,7 @@ export interface BannerProps { } const BannerAlert: React.FC = ({ - color = Colors.red, + color = Colors.danger, className = '', children, buttonLabel = 'See Errors', @@ -29,7 +29,7 @@ const BannerAlert: React.FC = ({ return 'text-yellow-800 border-yellow-300 bg-yellow-50 dark:text-yellow-300 dark:border-yellow-800 dark:bg-gray-800'; case Colors.dark: return 'text-gray-800 border-gray-300 bg-gray-50 dark:text-gray-300 dark:border-gray-600 dark:bg-gray-800'; - case Colors.red: + case Colors.danger: return 'text-red-800 border-red-800 bg-white dark:text-red-400 dark:border-red-800 dark:bg-gray-800'; case Colors.gray: return 'text-gray-800 border-gray-300 bg-gray-50 dark:text-gray-300 dark:border-gray-600 dark:bg-gray-800'; diff --git a/src/ui/Button.tsx b/src/ui/Button.tsx index 0ce09fcf..174c9806 100644 --- a/src/ui/Button.tsx +++ b/src/ui/Button.tsx @@ -21,25 +21,12 @@ const Button: React.FC = ({ danger: "bg-danger hover:bg-danger-hover", success: "bg-success hover:bg-success-hover", blueCustom: "bg-blue-custom hover:bg-blue-custom-hover", - disabled: "bg-disabled", warning: "bg-warning hover:bg-warning-hover", dark: "bg-dark", - red: "bg-red", gray: "bg-gray", light: "bg-light", - grayCustom: "", - almondHover: '', - dangerHover: '', lightGray: '', - warningHover: '', - primaryActive: '', - primaryHover: '', - secondaryLight: '', - secondaryHover: '', - successHover: '', - sucessLight: '', white: '', - transparent: '' }; const isDisabled = props.disabled; diff --git a/src/ui/Card.tsx b/src/ui/Card.tsx index 8cb42b46..4261d334 100644 --- a/src/ui/Card.tsx +++ b/src/ui/Card.tsx @@ -34,23 +34,16 @@ type CardFooterProps = { const colorClasses: Record = { almond: "bg-almond", primary: "bg-primary", - transparent : "bg-transparent", - primaryHover: "hover:bg-primary-hover", secondary: "bg-secondary", - secondaryHover: "hover:bg-secondary-hover", danger: "bg-danger", - dangerHover: "hover:bg-danger-hover", - grayCustom: "bg-grayCustom", success: "bg-success", - successHover: "hover:bg-success-hover", - disabled: "bg-disabled", warning: "bg-warning", - warningHover: "hover:bg-warning-hover", dark: "bg-dark", - red: "bg-red", gray: "bg-gray", light: "bg-light", white: "bg-white", + [Colors.blueCustom]: '', + [Colors.lightGray]: '' }; const getColorClass = (color?: Colors) => color ? colorClasses[color] ?? "" : ""; diff --git a/src/ui/Popover.tsx b/src/ui/Popover.tsx index 8f9a2500..d3f5b16b 100644 --- a/src/ui/Popover.tsx +++ b/src/ui/Popover.tsx @@ -1,11 +1,12 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; type PopoverProps = { children: React.ReactNode; withOnClick: boolean; popover: React.ReactNode; - placement?: 'top' | 'right' | 'bottom' | 'left'; + placement?: 'top' | 'right' | 'bottom' | 'left' | 'bottom-left' | 'bottom-right'; className?: string; + backgroundColor?: string; }; const Popover: React.FC = ({ @@ -14,8 +15,11 @@ const Popover: React.FC = ({ withOnClick = false, placement = 'bottom', className = '', + backgroundColor = 'bg-secondary', }) => { const [isOpen, setIsOpen] = useState(false); + const popoverRef = useRef(null); + const triggerRef = useRef(null); const getPlacementClasses = (placement: string) => { switch (placement) { @@ -27,6 +31,10 @@ const Popover: React.FC = ({ return 'top-full left-1/2 transform -translate-x-1/2 mt-2'; case 'left': return 'right-full top-1/2 transform -translate-y-1/2 mr-2'; + case 'bottom-left': + return 'top-full left-0 transform mt-2'; + case 'bottom-right': + return 'top-full right-0 transform mt-2'; default: return 'top-full left-1/2 transform -translate-x-1/2 mt-2'; } @@ -36,14 +44,36 @@ const Popover: React.FC = ({ ? { onClick: () => setIsOpen(!isOpen) } : { onMouseEnter: () => setIsOpen(true), onMouseLeave: () => setIsOpen(false) }; + const handleOutsideClick = (event: MouseEvent) => { + if ( + popoverRef.current && !popoverRef.current.contains(event.target as Node) && + triggerRef.current && !triggerRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + useEffect(() => { + if (isOpen) { + document.addEventListener('mousedown', handleOutsideClick); + } else { + document.removeEventListener('mousedown', handleOutsideClick); + } + + return () => { + document.removeEventListener('mousedown', handleOutsideClick); + }; + }, [isOpen]); + return ( - + {children} {isOpen && ( {popover} diff --git a/src/ui/ProgressBar.tsx b/src/ui/ProgressBar.tsx index 8d264d8a..863812eb 100644 --- a/src/ui/ProgressBar.tsx +++ b/src/ui/ProgressBar.tsx @@ -1,4 +1,3 @@ - import React from 'react'; type ProgressIndicatorProps = { @@ -8,13 +7,15 @@ type ProgressIndicatorProps = { const ProgressBar: React.FC = ({ progress }) => { return ( - + + + {progress}% + - {progress}% ); }; diff --git a/src/ui/menu/DropDownButton.tsx b/src/ui/menu/DropDownButton.tsx index eb173d19..465e0a19 100644 --- a/src/ui/menu/DropDownButton.tsx +++ b/src/ui/menu/DropDownButton.tsx @@ -13,9 +13,11 @@ type DropdownButtonProps = { row: any; options: DropdownOption[]; buttonText?: string; + children?: React.ReactNode; + className?: string; }; -const DropdownButton: React.FC = ({ row, options, buttonText = "Action" }) => { +const DropdownButton: React.FC = ({ row, options, buttonText = "Action", children, className }) => { const dropdownRef = useRef(null); useEffect(() => { @@ -25,11 +27,11 @@ const DropdownButton: React.FC = ({ row, options, buttonTex }, []); const handleOptionClick = (option: DropdownOption) => { - option.action? option.action(row) : null; + option.action ? option.action(row) : null; }; return ( - + = ({ row, options, buttonTex className="hs-dropdown-menu transition-[opacity,margin] duration hs-dropdown-open:opacity-100 opacity-0 hidden bg-white shadow-md rounded-lg p-2 mt-2 dark:bg-slate-800 dark:border dark:border-gray-700" aria-labelledby="hs-dropdown-custom-trigger" > + {children} {options?.map((option, index) => ( = ({ row, options, buttonTex ); }; -export default DropdownButton; \ No newline at end of file +export default DropdownButton; diff --git a/src/ui/table/EditableCell.tsx b/src/ui/table/EditableCell.tsx index 0037652a..69a50e83 100644 --- a/src/ui/table/EditableCell.tsx +++ b/src/ui/table/EditableCell.tsx @@ -55,9 +55,6 @@ const EditableCell = ({ placeholder={placeholder} value={value ?? ""} onChange={onChange} - onClick={(e) => { - e.stopPropagation() - }} onBlur={onBlur} /> ) diff --git a/src/ui/table/Table.tsx b/src/ui/table/Table.tsx index 950600bf..cbea7aa9 100644 --- a/src/ui/table/Table.tsx +++ b/src/ui/table/Table.tsx @@ -39,6 +39,7 @@ type TableProps = { getRowStyles?: (row: TData) => React.CSSProperties | undefined; getRowClasses?: (row: TData) => string | undefined; onCellEdit?: (rowIndex: string | number, columnId: any, value: any) => void + getRowId?: (originalRow: TData, index: number) => string }; function Table({ @@ -61,6 +62,7 @@ function Table({ getRowStyles, getRowClasses = (row) => 'bg-indigo-50', onCellEdit = () => { }, + getRowId = (originalRow, index) => { return originalRow?.[id] ?? index } }: TableProps) { const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); @@ -115,7 +117,7 @@ function Table({ rowSelection: selectedRow, columnVisibility: columnVisibility }, - getRowId: (originalRow, index) => { return originalRow?.[id] ?? index}, + getRowId: getRowId, onPaginationChange: setPagination, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, @@ -160,7 +162,7 @@ function Table({ return ( - + {table.getHeaderGroups().map(headerGroup => ( @@ -169,7 +171,7 @@ function Table({ @@ -221,7 +223,7 @@ function Table({ {row.getVisibleCells().map((cell, cellIndex) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -230,6 +232,7 @@ function Table({ ))} + {data.length > 0 && table ? ( diff --git a/src/utils/actionsUtils.ts b/src/utils/actionsUtils.ts index 0bdd3972..296cbc04 100644 --- a/src/utils/actionsUtils.ts +++ b/src/utils/actionsUtils.ts @@ -16,11 +16,15 @@ export const addStudyIdToAnonymizeList = async (studyId: string) => { const study = await getStudy(studyId) const patient = await getPatient(study.parentPatient) store.dispatch(addStudyToAnonymizeList({ - patient : patient, + patient : { + newPatientId: null, + newPatientName: null, + originalPatient: patient + }, study: { newPatientId: null, newPatientName: null, - newAccessionNumber: null, + newAccessionNumber: "GaelO-Flow", newStudyDescription: null, originalStudy: study } diff --git a/src/utils/enums.ts b/src/utils/enums.ts index 9841a824..3bf5c04d 100644 --- a/src/utils/enums.ts +++ b/src/utils/enums.ts @@ -1,28 +1,14 @@ export enum Colors { almond = 'almond', - almondHover = 'almond-hover', - blueCustom = 'buel-custom', - blusCustomHover = 'buel-custom-hover', + blueCustom = 'blueCustom', danger = 'danger', - dangerHover = "danger-hover", dark = 'dark', - disabled = 'disabled', gray = 'gray', - grayCustom = 'gray-custom', light = 'light', lightGray = 'light-gray', warning = 'warning', - warningHover = 'warning-hover', primary = 'primary', - primaryActive = 'primary-active', - primaryHover = 'primary-hover', - red = 'red', secondary = 'secondary', - secondaryLight = 'secondary-light', - secondaryHover = 'secondary-hover', success = 'success', - successHover = 'success-hover', - sucessLight = 'sucess-light', white = 'white', - transparent = 'transparent' } diff --git a/src/utils/types.ts b/src/utils/types.ts index 95323f59..dd66e9d9 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -139,7 +139,7 @@ export type ProcessingJob = { progress: number; state: string; id: string; - results : Record + results: Record }; export type Peer = { @@ -206,8 +206,8 @@ type QuerySeries = { SeriesDescription?: string; SeriesNumber?: string; SeriesInstanceUID?: string; - NumberOfSeriesRelatedInstances? : string; - ProtocolName? :string; + NumberOfSeriesRelatedInstances?: string; + ProtocolName?: string; }; type Level = "Series" | "Study"; @@ -304,7 +304,7 @@ export type Instances = { instanceCreationTime: string | null; instanceNumber: string | null; sopInstanceUID: string | null; - numberOfFrames: string|null; + numberOfFrames: string | null; }; parentSeries: string; type: string; @@ -345,7 +345,7 @@ export type PatientModifyPayload = { force: boolean; synchronous: boolean; keepSource: boolean; - keep : string[]; + keep: string[]; }; export type OrthancResponse = { @@ -365,20 +365,26 @@ export type Study = { type: string; }; +export type AnonPatient = { + newPatientName: string, + newPatientId: string, + originalPatient: Patient +} + export type AnonStudy = { - newPatientName : string, - newPatientId : string, - newStudyDescription : string, - newAccessionNumber : string, - originalStudy : Study; + newPatientName: string, + newPatientId: string, + newStudyDescription: string, + newAccessionNumber: string, + originalStudy: Study; } export type StudyModifyPayload = { - replace: Partial; + replace: Partial; remove: string[]; removePrivateTags: boolean; force: boolean; - keep : string[]; + keep: string[]; synchronous: boolean; keepSource: boolean; }; @@ -390,13 +396,25 @@ export type SeriesModifyPayload = { keepSource: boolean; force: boolean; synchronous: boolean; - keep : string[]; + keep: string[]; }; export type Queue = { - progress : number - state : string - id : string - results : Record - userId : number + progress: number + state: string + id: string + results: Record + userId: number +} + +export type AnonItem = { + OrthancStudyID: string, + Profile: 'full' | 'default', + NewAccessionNumber: string + NewPatientID: string + NewPatientName: string +} + +export type AnonymizePayload = { + Anonymizes: AnonItem[] } \ No newline at end of file diff --git a/src/welcome/ForgotPasswordForm.tsx b/src/welcome/ForgotPasswordForm.tsx index bf7b44a6..f8f3c1bd 100644 --- a/src/welcome/ForgotPasswordForm.tsx +++ b/src/welcome/ForgotPasswordForm.tsx @@ -55,9 +55,9 @@ const ForgotPasswordForm = () => { /> Connect diff --git a/tailwind.config.js b/tailwind.config.js index 4456e4fb..5af94d0a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -52,7 +52,6 @@ export default { 'primary-light': '#8C8BD3', 'secondary': '#EB9124', 'secondary-hover': '#BD6800', - 'secondaryLight': '#F1EBE8', 'danger': '#DB1F22', 'danger-hover': '#8A1315', 'sucess-light': '#CBDCD4', @@ -62,14 +61,11 @@ export default { 'warning': '#DDAD00', 'warning-hover': '#A88400', 'dark': '#484544', - 'redCustom': '#f8d7da', - 'blueCustom': '#0595a0', - 'blue-custom-hover': '#0a7e87', - 'grayCustom': '#D4CFCF', + 'blue-custom': '#2F77BA', + 'blue-custom-hover': '#245D92', + 'gray-custom': '#D4CFCF', 'light': '#FFFFFF', 'light-gray': '#EFEFEF', - 'badge-grayCustom': '#E6E6F2', - 'badge-blue-text': '#4A4AFF', }, boxShadow: { 'custom': '5px 17px 15px rgba(58, 51, 51, 0.20)',
{progress}%