diff --git a/snprc_ehr/resources/web/snprc_ehr/snprcReports.js b/snprc_ehr/resources/web/snprc_ehr/snprcReports.js index 0ba6c44f..c6db8514 100644 --- a/snprc_ehr/resources/web/snprc_ehr/snprcReports.js +++ b/snprc_ehr/resources/web/snprc_ehr/snprcReports.js @@ -568,7 +568,8 @@ EHR.reports.SndEvents = function (panel, tab) { filterConfig: JSON.stringify({ filters: tab.filters }), - hasPermission: userPermsInfo.container.effectivePermissions.includes('org.labkey.api.security.permissions.AdminPermission') + hasReadPermission: userPermsInfo.container.effectivePermissions.includes('org.labkey.snd.security.permissions.SNDViewerPermission'), + hasWritePermission: userPermsInfo.container.effectivePermissions.includes('org.labkey.snd.security.permissions.SNDEditorPermission') } const wp = new LABKEY.WebPart({ diff --git a/snprc_ehr/src/client/SndEventsWidget/SndEventsWidget.tsx b/snprc_ehr/src/client/SndEventsWidget/SndEventsWidget.tsx index 76ecad25..1b442644 100644 --- a/snprc_ehr/src/client/SndEventsWidget/SndEventsWidget.tsx +++ b/snprc_ehr/src/client/SndEventsWidget/SndEventsWidget.tsx @@ -2,23 +2,24 @@ import React, { FC, memo, useState, useEffect } from 'react'; import { EventListingGridPanel } from './components/EventListingGridPanel'; import './styles/sndEventsWidget.scss'; import { FormGroup, ControlLabel, FormControl } from 'react-bootstrap'; -import { getMultiRow } from './actions'; +import { getMultiRow } from './actions/actions'; import { Alert } from '@labkey/components'; interface Props { filterConfig: any, - hasPermission?: boolean + hasReadPermission: boolean, + hasWritePermission: boolean } export const SndEventsWidget: FC = memo((props: Props) => { - const {filterConfig, hasPermission} = props; + const {filterConfig, hasReadPermission, hasWritePermission} = props; const [subjectIds, setSubjectIds] = useState(['']); const [message, setMessage] = useState(undefined); const [status, setStatus] = useState(undefined); useEffect(() => { (async () => { - if (hasPermission && filterConfig !== undefined && filterConfig.length != 0) { + if (hasReadPermission && filterConfig !== undefined && filterConfig.length != 0) { await getSubjectIdsFromFilters(filterConfig, setSubjectIds); } })(); @@ -62,24 +63,24 @@ export const SndEventsWidget: FC = memo((props: Props) => { return (
- {!hasPermission && ( + {!hasReadPermission && ( User Does not have permission to view this panel )} - {hasPermission && (filterConfig === undefined || filterConfig.length == 0) && ( + {hasReadPermission && (filterConfig === undefined || filterConfig.length == 0) && ( form() ) } - {hasPermission && (filterConfig !== undefined && filterConfig.length != 0 && filterConfig?.filters.inputType === 'none') && ( + {hasReadPermission && (filterConfig !== undefined && filterConfig.length != 0 && filterConfig?.filters.inputType === 'none') && ( 'Entire Database' filter is not supported for this query. )} - {hasPermission && subjectIds[0] === 'none' && ( + {hasReadPermission && subjectIds[0] === 'none' && ( No animals were found for filter selections )} {message && ( {message} )} - {hasPermission && subjectIds && ( - + {hasReadPermission && subjectIds && ( + )}
diff --git a/snprc_ehr/src/client/SndEventsWidget/actions.ts b/snprc_ehr/src/client/SndEventsWidget/actions/actions.ts similarity index 100% rename from snprc_ehr/src/client/SndEventsWidget/actions.ts rename to snprc_ehr/src/client/SndEventsWidget/actions/actions.ts diff --git a/snprc_ehr/src/client/SndEventsWidget/actions/fetchEvent.ts b/snprc_ehr/src/client/SndEventsWidget/actions/fetchEvent.ts new file mode 100644 index 00000000..f4df3c48 --- /dev/null +++ b/snprc_ehr/src/client/SndEventsWidget/actions/fetchEvent.ts @@ -0,0 +1,80 @@ +import {ActionURL, getServerContext } from "@labkey/api"; + +export const fetchEvent = async (eventID: string | undefined): Promise => { + const url = ActionURL.buildURL('snd', 'getEvent.api') + const headers = { + 'Content-Type': 'application/json', + accept: 'application/json', + 'X-LABKEY-CSRF': getServerContext().CSRF + } + + const body = JSON.stringify({ + eventId: eventID, + getTextNarrative: false, + getRedactedNarrative: false, + getHtmlNarrative: true, + getRedactedHtmlNarrative: false, + }) + + return fetch(url, { + headers, + body, + method: 'post', + credentials: 'include', + }).then(async response => { + const data = await response.json() + if (!data.success) { + throw new Error(data.event.exception.message) + } + if(response.status === 200) + return data.event + }) +} + +export type FetchAnimalEventResponse = { + date: string + eventData: SuperPackageEventData[] + eventId: number + extraFields: Field[] + htmlNarrative: string + note: string + projectIdRev: string + qcState: QCState + redactedHtmlNarrative: string + subjectId: string + textNarrative: string +} + +type SuperPackageEventData = { + attributes: [] + eventDataId: number + extraFields: Field[] + narrativeTemplate: string + subPackages: SuperPackageEventData[] + superPkgId: number +} + +type Field = { +} + +type QCState = 'Completed' | 'In Progress' | 'Review Required' | 'Rejected' + +type ResponseError = { + exception: string +} + +export class ResponseHadErrorException extends Error { + stackTrace: string[] + + constructor({ exception, exceptionClass, stackTrace }: ResponseWithError) { + super(exception) + this.name = exceptionClass + this.stackTrace = stackTrace + } +} + +interface ResponseWithError { + exception: string + exceptionClass: string + stackTrace: string[] +} \ No newline at end of file diff --git a/snprc_ehr/src/client/SndEventsWidget/actions/saveEvent.ts b/snprc_ehr/src/client/SndEventsWidget/actions/saveEvent.ts new file mode 100644 index 00000000..b3ee5e90 --- /dev/null +++ b/snprc_ehr/src/client/SndEventsWidget/actions/saveEvent.ts @@ -0,0 +1,89 @@ +import { ActionURL, getServerContext } from '@labkey/api'; + +export const saveEvent = async (eventToSave: EventToSave): Promise => { + const url = ActionURL.buildURL('snd', 'saveEvent.api').replace('.org//', '.org/') + + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify(eventToSave), + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json', + 'X-LABKEY-CSRF': getServerContext().CSRF, + }, + }) + + const json = await response.json() + + if (response?.status === 500) { + throw new LabkeyError(json) + } + + if (response?.status !== 200) { + // @todo, investigate if this is needed + throw new LabkeyError(json) + } + + if (!json.success) { + throw new LabkeyError(json) + } + + return json +} + +export interface EventToSave { + eventId?: number, + date: string, // YYYY-MM-DDTHH:mm:ss + projectIdRev: string + subjectId: string, + qcState: QCState, + note?: string, + extraFields?: PropertyDescriptor[], + eventData: EventData[] +} + +export interface SaveEventResponse { + success: boolean + event: Event +} + +class LabkeyError extends Error { + constructor({ exception, exceptionClass, stackTrace }) { + const message = ` + ${exception} + + ${stackTrace.join('\n\t').toString()} + `.trim() + + super(message) + this.name = exceptionClass + this.stack = stackTrace + } +} + +interface EventData { + eventDataId?: number, + exception?: Exception + superPkgId: number, + narrativeTemplate?: string, + extraFields?: PropertyDescriptor[], + attributes: Attribute[] + subPackages: EventData[] +} + +export interface Attribute { + propertyId: number, + propertyName?: string, + value: string | number, + propertyDescriptor?: PropertyDescriptor + exception?: Exception +} + + +interface Exception { + message: string, + severity: string +} + +type QCState = 'Completed' | 'Rejected' | 'Review Required' | 'In Progress' \ No newline at end of file diff --git a/snprc_ehr/src/client/SndEventsWidget/app.tsx b/snprc_ehr/src/client/SndEventsWidget/app.tsx index 562b685d..cfcc80cd 100644 --- a/snprc_ehr/src/client/SndEventsWidget/app.tsx +++ b/snprc_ehr/src/client/SndEventsWidget/app.tsx @@ -4,6 +4,6 @@ import {SndEventsWidget} from "./SndEventsWidget"; // Need to wait for container element to be available in labkey wrapper before render window.addEventListener('DOMContentLoaded', (event) => { - const config = {filterConfig: [], hasPermission: true} + const config = {filterConfig: [], hasReadPermission: true, hasWritePermission: true } ReactDOM.render(, document.getElementById('app')); }); \ No newline at end of file diff --git a/snprc_ehr/src/client/SndEventsWidget/components/AdmissionInfoPopover.tsx b/snprc_ehr/src/client/SndEventsWidget/components/AdmissionInfoPopover.tsx index b971e635..60a13231 100644 --- a/snprc_ehr/src/client/SndEventsWidget/components/AdmissionInfoPopover.tsx +++ b/snprc_ehr/src/client/SndEventsWidget/components/AdmissionInfoPopover.tsx @@ -1,6 +1,6 @@ import React, { FC, memo, useRef, useState } from 'react'; import { OverlayTrigger, Popover } from 'react-bootstrap'; -import { getTableRow } from '../actions'; +import { getTableRow } from '../actions/actions'; interface Props { admitChargeId: string, diff --git a/snprc_ehr/src/client/SndEventsWidget/components/DeleteModal.tsx b/snprc_ehr/src/client/SndEventsWidget/components/DeleteModal.tsx new file mode 100644 index 00000000..893b33b5 --- /dev/null +++ b/snprc_ehr/src/client/SndEventsWidget/components/DeleteModal.tsx @@ -0,0 +1,71 @@ +import React, { FC, memo, ReactNode, useState } from 'react'; +import { ConfirmModal, resolveErrorMessage } from '@labkey/components'; +import { FetchAnimalEventResponse, fetchEvent } from '../actions/fetchEvent'; +import { EventToSave, saveEvent } from '../actions/saveEvent'; + +interface Props { + onCancel: () => any, + onComplete: (message, status) => any, + onError: (message) => any, + eventId: string, +} +export const DeleteModal: FC = memo((props: Props) => { + const {onCancel, onComplete, onError, eventId } = props; + const [error, setError] = useState(undefined); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleDelete = async () => { + setIsSubmitting(true); + + const event: FetchAnimalEventResponse | void = await fetchEvent(eventId).catch(error => { + console.error(error); + setError(resolveErrorMessage(error, "Event", "Event" + 's', 'delete')); + setIsSubmitting(false); + }); + + if (event) { + const deleteRequest: EventToSave = { + eventId: event.eventId, + date: event.date, + projectIdRev: event.projectIdRev, + subjectId: event.subjectId, + qcState: event.qcState, + note: '', + extraFields: event.extraFields, + eventData: [] + } + + saveEvent(deleteRequest) + .then(t => { + if (t.success) { + onComplete('Successfully deleted Event', 'success'); + } else { + onComplete('There was a problem deleting the Event', 'danger'); + } + + }) + .catch(error => { + console.error(error); + setError(resolveErrorMessage(error, "Event", "Event" + 's', 'delete')); + setIsSubmitting(false); + }); + } + + } + + return ( +
+ +

Event for this Procedure will be permanently deleted. Do you want to proceed?

+
+
+ ) +}) \ No newline at end of file diff --git a/snprc_ehr/src/client/SndEventsWidget/components/EventListingGridPanel.tsx b/snprc_ehr/src/client/SndEventsWidget/components/EventListingGridPanel.tsx index 5c6ff23a..612d5377 100644 --- a/snprc_ehr/src/client/SndEventsWidget/components/EventListingGridPanel.tsx +++ b/snprc_ehr/src/client/SndEventsWidget/components/EventListingGridPanel.tsx @@ -12,15 +12,17 @@ import { SCHEMAS } from '../schemas'; import { produce } from 'immer'; import { ProcedureEntryModal } from './ProcedureEntryModal'; import { AdmissionInfoPopover } from './AdmissionInfoPopover'; +import { DeleteModal } from './DeleteModal'; interface EventListingProps { subjectIDs: string[], onChange: (message: string, status: string) => any, - onError: (message: string) => any + onError: (message: string) => any, + hasWritePermission: boolean } export const EventListingGridPanelImpl: FC = memo((props: EventListingProps & InjectedQueryModels) => { - const {subjectIDs, actions, queryModels, onChange, onError} = props; + const {subjectIDs, actions, queryModels, onChange, onError, hasWritePermission} = props; const [showDialog, setShowDialog] = useState(''); const [eventID, setEventID] = useState(''); @@ -89,10 +91,14 @@ export const EventListingGridPanelImpl: FC = memo((props: Eve ); }; - const handleClick = (value) => { + const handleClick = (value, isDelete: boolean) => { // Code to run when the button is clicked setEventID(value.toString()); - toggleDialog('edit'); + if (isDelete) { + toggleDialog('delete'); + } else { + toggleDialog('edit'); + } }; const handleCloseUpdateModal = (message?: string, status?: string) => { @@ -110,11 +116,20 @@ export const EventListingGridPanelImpl: FC = memo((props: Eve return (
{data.get('value')} - + {hasWritePermission && ( + + )} + {hasWritePermission && ( + + )}
); }; @@ -133,7 +148,7 @@ export const EventListingGridPanelImpl: FC = memo((props: Eve const renderButtons = () => { return (
- {} @@ -174,6 +189,12 @@ export const EventListingGridPanelImpl: FC = memo((props: Eve onComplete={handleCloseUpdateModal} subjectId={subjectIDs[0]}/> )} + {showDialog === 'delete' && ( + + )}
); diff --git a/snprc_ehr/src/client/SndEventsWidget/dev.tsx b/snprc_ehr/src/client/SndEventsWidget/dev.tsx index 8b2835be..d04fe60b 100644 --- a/snprc_ehr/src/client/SndEventsWidget/dev.tsx +++ b/snprc_ehr/src/client/SndEventsWidget/dev.tsx @@ -4,7 +4,7 @@ import { AppContainer } from 'react-hot-loader'; import {SndEventsWidget} from "./SndEventsWidget"; const render = () => { - const config = {filterConfig: [], hasPermission: true} + const config = {filterConfig: [], hasReadPermission: true, hasWritePermission: true } ReactDOM.render( diff --git a/snprc_ehr/src/client/SndEventsWidget/styles/sndEventsWidget.scss b/snprc_ehr/src/client/SndEventsWidget/styles/sndEventsWidget.scss index 8477412d..b63beb93 100644 --- a/snprc_ehr/src/client/SndEventsWidget/styles/sndEventsWidget.scss +++ b/snprc_ehr/src/client/SndEventsWidget/styles/sndEventsWidget.scss @@ -38,6 +38,24 @@ display: table; } +//.modal-content { +// font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif; +// position: relative; +// left: 7%; +// width: 85%; +// border-radius: 5px; +// transform: translate(0, 10%) +//} +// +// +//.modal-header { +// font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif; +// border-bottom: 1px solid #ddd; +// border-top-left-radius: 5px; +// border-top-right-radius: 5px; +//} + + .grid-panel__grid .table-responsive { //overflow-y: scroll; width: 100%; @@ -196,6 +214,12 @@ background: none; } +.trash-btn { + color: #0479a8; + border: none; + background: none; +} + .fade { opacity: unset; } diff --git a/snprc_ehr/src/client/SndEventsWidget/webpart/app.tsx b/snprc_ehr/src/client/SndEventsWidget/webpart/app.tsx index 9e185a8f..eff52f4b 100644 --- a/snprc_ehr/src/client/SndEventsWidget/webpart/app.tsx +++ b/snprc_ehr/src/client/SndEventsWidget/webpart/app.tsx @@ -10,7 +10,8 @@ App.registerApp('SndEventsWidgetWebpart', (target, ctx) => { const config: configProps = { ...ctx, filterConfig: ctx.filterConfig ? JSON.parse(ctx.filterConfig) : undefined, - hasPermission: ctx.hasPermission ? JSON.parse(ctx.hasPermission) : undefined + hasReadPermission: ctx.hasReadPermission ? JSON.parse(ctx.hasReadPermission) : undefined, + hasWritePermission: ctx.hasWritePermission ? JSON.parse(ctx.hasWritePermission) : undefined, } ReactDOM.render(, document.getElementById(target)); }); \ No newline at end of file diff --git a/snprc_ehr/src/client/SndEventsWidget/webpart/config.ts b/snprc_ehr/src/client/SndEventsWidget/webpart/config.ts index 82fb1246..b7dd44c7 100644 --- a/snprc_ehr/src/client/SndEventsWidget/webpart/config.ts +++ b/snprc_ehr/src/client/SndEventsWidget/webpart/config.ts @@ -1,5 +1,7 @@ export interface configProps { filterConfig: { filters: any; - } + }, + hasReadPermission: boolean, + hasWritePermission: boolean } \ No newline at end of file diff --git a/snprc_ehr/src/client/SndEventsWidget/webpart/dev.tsx b/snprc_ehr/src/client/SndEventsWidget/webpart/dev.tsx index a493c83f..7ddcf89a 100644 --- a/snprc_ehr/src/client/SndEventsWidget/webpart/dev.tsx +++ b/snprc_ehr/src/client/SndEventsWidget/webpart/dev.tsx @@ -10,7 +10,8 @@ App.registerApp('SndEventsWidgetWebpart', (target: string, ctx) => { const config: configProps = { ...ctx, filterConfig: ctx.filterConfig ? JSON.parse(ctx.filterConfig) : undefined, - hasPermission: ctx.hasPermission ? ctx.hasPermission : undefined + hasReadPermission: ctx.hasReadPermission ? ctx.hasReadPermission : undefined, + hasWritePermission: ctx.hasWritePermission ? ctx.hasWritePermission : undefined } ReactDOM.render( diff --git a/snprc_ehr/src/client/SndLookupsManagement/styles/sndLookupsManagement.scss b/snprc_ehr/src/client/SndLookupsManagement/styles/sndLookupsManagement.scss index b66f7b43..b309a25f 100644 --- a/snprc_ehr/src/client/SndLookupsManagement/styles/sndLookupsManagement.scss +++ b/snprc_ehr/src/client/SndLookupsManagement/styles/sndLookupsManagement.scss @@ -225,6 +225,7 @@ transform: translate(0, 10%) } + .modal-header { font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif; border-bottom: 1px solid #ddd;