Skip to content

Commit

Permalink
[JN-1088] Admin form UX improvements + new task type (#922)
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewBemis authored Jun 12, 2024
1 parent 6b4a5b0 commit 59a2ff9
Show file tree
Hide file tree
Showing 25 changed files with 449 additions and 205 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public enum SurveyType {
CONSENT, // for consent forms
RESEARCH, // for surveys intended to be included in the research dataset of a study
OUTREACH // for surveys intended for outreach purposes, e.g. to collect marketing/feedback information
OUTREACH, // for surveys intended for outreach purposes, e.g. to collect marketing/feedback information
ADMIN // for surveys intended for study staff purposes, e.g. for data entry
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ public enum TaskType {
CONSENT,
SURVEY, // a research survey
OUTREACH, // an outreach activity -- not essential for research
KIT_REQUEST
KIT_REQUEST,
ADMIN_FORM // a task for study staff to complete -- not visible to participants
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ public HubResponse<SurveyResponse> updateResponse(SurveyResponse responseDto, Re
SurveyResponse response = findOrCreateResponse(task, enrollee, enrollee.getParticipantUserId(), responseDto, portalId, operator);

List<Answer> updatedAnswers = createOrUpdateAnswers(responseDto.getAnswers(), response, survey, ppUser);
List<Answer> allAnswers = new ArrayList<>(response.getAnswers());
List<Answer> existingAnswers = answerService.findByResponse(response.getId());
allAnswers.addAll(existingAnswers);
response.setAnswers(allAnswers);

DataAuditInfo auditInfo = DataAuditInfo.builder()
.enrolleeId(enrollee.getId())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import bio.terra.pearl.core.service.survey.event.SurveyPublishedEvent;
import bio.terra.pearl.core.service.workflow.*;
import lombok.extern.slf4j.Slf4j;
import okhttp3.internal.concurrent.Task;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -185,7 +184,7 @@ public void handleSurveyPublished(SurveyPublishedEvent event) {
}
if (event.getSurvey().isAssignToExistingEnrollees()) {
ParticipantTaskAssignDto assignDto = new ParticipantTaskAssignDto(
TaskType.SURVEY,
taskTypeForSurveyType.get(event.getSurvey().getSurveyType()),
event.getSurvey().getStableId(),
event.getSurvey().getVersion(),
null,
Expand Down Expand Up @@ -248,7 +247,8 @@ public ParticipantTask buildTask(Enrollee enrollee, PortalParticipantUser portal
private final Map<SurveyType, TaskType> taskTypeForSurveyType = Map.of(
SurveyType.CONSENT, TaskType.CONSENT,
SurveyType.RESEARCH, TaskType.SURVEY,
SurveyType.OUTREACH, TaskType.OUTREACH
SurveyType.OUTREACH, TaskType.OUTREACH,
SurveyType.ADMIN, TaskType.ADMIN_FORM
);

/**
Expand Down
81 changes: 51 additions & 30 deletions ui-admin/src/study/StudyContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,50 +53,49 @@ function StudyContent({ studyEnvContext }: {studyEnvContext: StudyEnvContextT})
}, { setIsLoading })
}

const researchSurveyStableIds = _uniq(configuredSurveys
.filter(configSurvey => configSurvey.survey.surveyType === 'RESEARCH')
.sort((a, b) => a.surveyOrder - b.surveyOrder)
.map(configSurvey => configSurvey.survey.stableId))
const outreachSurveyStableIds = _uniq(configuredSurveys
.filter(configSurvey => configSurvey.survey.surveyType === 'OUTREACH')
.sort((a, b) => a.surveyOrder - b.surveyOrder)
.map(configSurvey => configSurvey.survey.stableId))
const consentSurveyStableIds = _uniq(configuredSurveys
.filter(configSurvey => configSurvey.survey.surveyType === 'CONSENT')
.sort((a, b) => a.surveyOrder - b.surveyOrder)
.map(configSurvey => configSurvey.survey.stableId))
function getUniqueStableIdsForType(configuredSurveys: StudyEnvironmentSurveyNamed[], surveyType: string) {
return _uniq(configuredSurveys
.filter(configSurvey => configSurvey.survey.surveyType === surveyType)
.sort((a, b) => a.surveyOrder - b.surveyOrder)
.map(configSurvey => configSurvey.survey.stableId))
}

const researchSurveyStableIds = getUniqueStableIdsForType(configuredSurveys, 'RESEARCH')
const outreachSurveyStableIds = getUniqueStableIdsForType(configuredSurveys, 'OUTREACH')
const consentSurveyStableIds = getUniqueStableIdsForType(configuredSurveys, 'CONSENT')
const adminFormStableIds = getUniqueStableIdsForType(configuredSurveys, 'ADMIN')

return <div className="container-fluid px-4 py-2">
{ renderPageHeader('Forms & Surveys') }
<LoadingSpinner isLoading={isLoading}>
<div className="col-12">
{ currentEnv.studyEnvironmentConfig.initialized && <ul className="list-unstyled">
<li className="mb-3 rounded-2 p-3" style={{ background: '#efefef' }}>
<h6>Pre-enrollment questionnaire</h6>
<h6>Pre-enrollment Questionnaire</h6>
<div className="flex-grow-1 pt-3">
{ preEnrollSurvey && <ul className="list-unstyled">
{preEnrollSurvey && <ul className="list-unstyled">
<li className="d-flex align-items-center">
<Link to={`preEnroll/${preEnrollSurvey.stableId}?readOnly=${isReadOnlyEnv}`}>
{preEnrollSurvey.name} <span className="detail">v{preEnrollSurvey.version}</span>
</Link>

{ !isReadOnlyEnv && <div className="nav-item dropdown ms-1">
<IconButton icon={faEllipsisH} data-bs-toggle="dropdown"
{!isReadOnlyEnv && <div className="nav-item dropdown ms-1">
<IconButton icon={faEllipsisH} data-bs-toggle="dropdown"
aria-expanded="false" aria-label="configure pre-enroll menu"/>
<div className="dropdown-menu">
<ul className="list-unstyled">
<li>
<button className="dropdown-item"
onClick={() => alert('To remove a pre-enroll survey, contact support')}>
Remove
Remove
</button>
</li>
</ul>
</div>
</div> }
</div>}
</li>
</ul>}
{ (!preEnrollSurvey && !isReadOnlyEnv) && <Button variant="secondary"
{(!preEnrollSurvey && !isReadOnlyEnv) && <Button variant="secondary"
data-testid={'addPreEnroll'}
onClick={() => {
setShowCreatePreEnrollModal(!showCreatePreEnrollSurveyModal)
Expand All @@ -107,7 +106,7 @@ function StudyContent({ studyEnvContext }: {studyEnvContext: StudyEnvContextT})
</div>
</li>
<li className="mb-3 rounded-2 p-3" style={{ background: '#efefef' }}>
<h2 className="h6">Consent forms</h2>
<h2 className="h6">Consent Forms</h2>
<div className="flex-grow-1 pt-3">
<SurveyEnvironmentTable stableIds={consentSurveyStableIds}
studyEnvParams={paramsFromContext(studyEnvContext)}
Expand Down Expand Up @@ -150,6 +149,28 @@ function StudyContent({ studyEnvContext }: {studyEnvContext: StudyEnvContextT})
</div>
</div>
</li>
<li className="mb-3 rounded-2 p-3" style={{ background: '#efefef' }}>
<h6>Study Staff Forms</h6>
<div className="flex-grow-1 pt-3">
<SurveyEnvironmentTable stableIds={adminFormStableIds}
studyEnvParams={paramsFromContext(studyEnvContext)}
configuredSurveys={configuredSurveys}
setSelectedSurveyConfig={setSelectedSurveyConfig}
updateConfiguredSurvey={updateConfiguredSurvey}
setShowDeleteSurveyModal={setShowDeleteSurveyModal}
setShowArchiveSurveyModal={setShowArchiveSurveyModal}
showArchiveSurveyModal={showArchiveSurveyModal}
showDeleteSurveyModal={showDeleteSurveyModal}
/>
<div>
<Button variant="secondary" data-testid={'addAdminForm'} onClick={() => {
setCreateSurveyType('ADMIN')
}}>
<FontAwesomeIcon icon={faPlus}/> Add
</Button>
</div>
</div>
</li>
<li className="mb-3 rounded-2 p-3" style={{ background: '#efefef' }}>
<h6>Outreach</h6>
<div className="flex-grow-1 pt-3">
Expand All @@ -172,18 +193,18 @@ function StudyContent({ studyEnvContext }: {studyEnvContext: StudyEnvContextT})
</div>
</div>
</li>
</ul> }
{ createSurveyType && <CreateSurveyModal studyEnvContext={studyEnvContext} type={createSurveyType}
onDismiss={() => setCreateSurveyType(undefined)}/> }
{ (showArchiveSurveyModal && selectedSurveyConfig) && <ArchiveSurveyModal studyEnvContext={studyEnvContext}
</ul>}
{createSurveyType && <CreateSurveyModal studyEnvContext={studyEnvContext} type={createSurveyType}
onDismiss={() => setCreateSurveyType(undefined)}/>}
{(showArchiveSurveyModal && selectedSurveyConfig) && <ArchiveSurveyModal studyEnvContext={studyEnvContext}
selectedSurveyConfig={selectedSurveyConfig}
onDismiss={() => setShowArchiveSurveyModal(false)}/> }
{ (showDeleteSurveyModal && selectedSurveyConfig) && <DeleteSurveyModal studyEnvContext={studyEnvContext}
onDismiss={() => setShowArchiveSurveyModal(false)}/>}
{(showDeleteSurveyModal && selectedSurveyConfig) && <DeleteSurveyModal studyEnvContext={studyEnvContext}
selectedSurveyConfig={selectedSurveyConfig}
onDismiss={() => setShowDeleteSurveyModal(false)}/> }
{ showCreatePreEnrollSurveyModal && <CreatePreEnrollSurveyModal studyEnvContext={studyEnvContext}
onDismiss={() => setShowCreatePreEnrollModal(false)}/> }
{ !currentEnv.studyEnvironmentConfig.initialized && <div>Not yet initialized</div> }
onDismiss={() => setShowDeleteSurveyModal(false)}/>}
{showCreatePreEnrollSurveyModal && <CreatePreEnrollSurveyModal studyEnvContext={studyEnvContext}
onDismiss={() => setShowCreatePreEnrollModal(false)}/>}
{!currentEnv.studyEnvironmentConfig.initialized && <div>Not yet initialized</div>}
</div>
</LoadingSpinner>
</div>
Expand Down
103 changes: 72 additions & 31 deletions ui-admin/src/study/participants/enrolleeView/EnrolleeView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import { ParticipantTask, StudyEnvironmentSurvey, SurveyResponse } from 'api/api'
import { StudyEnvContextT } from 'study/StudyEnvironmentRouter'
import { Link, NavLink, Route, Routes } from 'react-router-dom'
Expand Down Expand Up @@ -35,41 +35,59 @@ export default function EnrolleeView({ studyEnvContext }: { studyEnvContext: Stu
const { isLoading, enrollee, reload } = useRoutedEnrollee(studyEnvContext)
return <>
{isLoading && <LoadingSpinner/>}
{!isLoading && enrollee && <LoadedEnrolleeView enrollee={enrollee} studyEnvContext={studyEnvContext}
onUpdate={reload}/>}
{!isLoading && enrollee &&
<LoadedEnrolleeView enrollee={enrollee} studyEnvContext={studyEnvContext} onUpdate={reload}/>}
</>
}

/** shows a master-detail view for an enrollee with sub views on surveys, tasks, etc... */
export function LoadedEnrolleeView({ enrollee, studyEnvContext, onUpdate }:
{ enrollee: Enrollee, studyEnvContext: StudyEnvContextT, onUpdate: () => void }) {
export function LoadedEnrolleeView({ enrollee, studyEnvContext, onUpdate }: {
enrollee: Enrollee, studyEnvContext: StudyEnvContextT, onUpdate: () => void
}) {
const { currentEnv, currentEnvPath } = studyEnvContext
const [responseMap, setResponseMap] = useState<ResponseMapT>({})

const surveys: StudyEnvironmentSurvey[] = currentEnv.configuredSurveys
const responseMap: ResponseMapT = {}
surveys.forEach(configSurvey => {
// to match responses to surveys, filter using the tasks, since those have the stableIds
// this is valid since it's currently enforced that all survey responses are done as part of a task,
const matchedTask = enrollee.participantTasks
.find(task => task.targetStableId === configSurvey.survey.stableId)
if (!matchedTask) {
return
}
const matchedResponse = enrollee.surveyResponses
.find(response => matchedTask.surveyResponseId === response.id)
responseMap[configSurvey.survey.stableId] = {
survey: configSurvey,
response: matchedResponse,
task: matchedTask
}
})

const researchSurveys = surveys
.filter(survey => survey.survey.surveyType === 'RESEARCH')
const outreachSurveys = surveys
.filter(survey => survey.survey.surveyType === 'OUTREACH')
const consentSurveys = surveys
.filter(survey => survey.survey.surveyType === 'CONSENT')
const adminSurveys = surveys
.filter(survey => survey.survey.surveyType === 'ADMIN')

const updateResponseMap = (stableId: string, response: SurveyResponse) => {
setResponseMap({
...responseMap,
[stableId]: {
...responseMap[stableId],
response
}
})
}

useEffect(() => {
const updatedResponseMap: ResponseMapT = {}
surveys.forEach(configSurvey => {
// to match responses to surveys, filter using the tasks, since those have the stableIds
// this is valid since it's currently enforced that all survey responses are done as part of a task,
const matchedTask = enrollee.participantTasks
.find(task => task.targetStableId === configSurvey.survey.stableId)
if (!matchedTask) {
return
}
const matchedResponse = enrollee.surveyResponses
.find(response => matchedTask.surveyResponseId === response.id)
updatedResponseMap[configSurvey.survey.stableId] = {
survey: configSurvey,
response: matchedResponse,
task: matchedTask
}
})
setResponseMap(updatedResponseMap)
}, [enrollee])

return <div className="ParticipantView mt-3 ps-4">
<NavBreadcrumb value={enrollee?.shortcode || ''}>
Expand All @@ -90,7 +108,7 @@ export function LoadedEnrolleeView({ enrollee, studyEnvContext, onUpdate }:
<div style={navDivStyle}>
<ul className="list-unstyled">
<li style={navListItemStyle} className="ps-3">
<NavLink to="." className={getLinkCssClasses}>Overview</NavLink>
<NavLink end to="." className={getLinkCssClasses}>Overview</NavLink>
</li>
<li style={navListItemStyle} className="ps-3">
<NavLink to="profile" className={getLinkCssClasses}>Profile</NavLink>
Expand All @@ -100,7 +118,7 @@ export function LoadedEnrolleeView({ enrollee, studyEnvContext, onUpdate }:
<ul className="list-unstyled">
{currentEnv.preEnrollSurvey && <li className="mb-2">
<NavLink to="preRegistration" className={getLinkCssClasses}>
PreEnrollment
PreEnrollment
</NavLink>
</li>}
{consentSurveys.map(survey => {
Expand All @@ -114,7 +132,7 @@ export function LoadedEnrolleeView({ enrollee, studyEnvContext, onUpdate }:
</ul>}/>
</li>
<li style={navListItemStyle}>
<CollapsableMenu header={'Surveys'} headerClass="text-black" content={
<CollapsableMenu header={'Research Surveys'} headerClass="text-black" content={
<ul className="list-unstyled">
{researchSurveys.map(survey => {
const stableId = survey.survey.stableId
Expand All @@ -127,9 +145,29 @@ export function LoadedEnrolleeView({ enrollee, studyEnvContext, onUpdate }:
</ul>}
/>
</li>
<li style={navListItemStyle}>
<CollapsableMenu header={'Study Staff Forms'} headerClass="text-black" content={
<ul className="list-unstyled">
{adminSurveys.length === 0 && <li className="mb-2">
<span className="text-muted fst-italic">No study staff forms</span>
</li>}
{adminSurveys.map(survey => {
const stableId = survey.survey.stableId
return <li className="mb-2 d-flex justify-content-between
align-items-center" key={stableId}>
{createSurveyNavLink(stableId, responseMap, survey)}
{badgeForResponses(responseMap[stableId]?.response)}
</li>
})}
</ul>}
/>
</li>
<li style={navListItemStyle}>
<CollapsableMenu header={'Outreach'} headerClass="text-black" content={
<ul className="list-unstyled">
{outreachSurveys.length === 0 && <li className="mb-2">
<span className="text-muted fst-italic">No outreach opportunities</span>
</li>}
{outreachSurveys.map(survey => {
const stableId = survey.survey.stableId
return <li className="mb-2 d-flex justify-content-between
Expand All @@ -146,10 +184,9 @@ export function LoadedEnrolleeView({ enrollee, studyEnvContext, onUpdate }:
Kit requests
</NavLink>
{
enrollee.kitRequests.length > 0 &&
<span className="badge align-middle bg-secondary ms-1 mb-1">
{enrollee.kitRequests.length}
</span>
<span className="badge align-middle bg-secondary ms-1 mb-1">
{enrollee.kitRequests.length}
</span>
}
</li>
<li style={navListItemStyle}>
Expand Down Expand Up @@ -180,11 +217,15 @@ export function LoadedEnrolleeView({ enrollee, studyEnvContext, onUpdate }:
onUpdate={onUpdate}/>}/>
{currentEnv.preEnrollSurvey && <Route path="preRegistration/*" element={
<PreEnrollmentView preEnrollSurvey={currentEnv.preEnrollSurvey}
preEnrollResponse={enrollee.preEnrollmentResponse} studyEnvContext={studyEnvContext}/>
preEnrollResponse={enrollee.preEnrollmentResponse}
studyEnvContext={studyEnvContext}/>
}/>}
<Route path="surveys">
<Route path=":surveyStableId/*" element={<SurveyResponseView enrollee={enrollee}
responseMap={responseMap} studyEnvContext={studyEnvContext} onUpdate={onUpdate}/>}/>
responseMap={responseMap}
updateResponseMap={updateResponseMap}
studyEnvContext={studyEnvContext}
onUpdate={onUpdate}/>}/>
<Route path="*" element={<div>Unknown participant survey page</div>}/>
</Route>
<Route path="tasks" element={<ParticipantTaskView enrollee={enrollee}/>}/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,14 @@ function ParticipantList({ studyEnvContext }: {studyEnvContext: StudyEnvContextT
}, {
id: 'familyName',
header: 'Family name',
accessorKey: 'profile.familyName',
accessorFn: row => row.profile?.familyName,
meta: {
columnType: 'string'
}
}, {
id: 'givenName',
header: 'Given name',
accessorKey: 'profile.givenName',
accessorFn: row => row.profile?.givenName,
meta: {
columnType: 'string'
}
Expand Down
Loading

0 comments on commit 59a2ff9

Please sign in to comment.