Skip to content

Commit

Permalink
feat(datatrakWeb): RN-1243: resubmit surveys via Datatrak Web (#5638)
Browse files Browse the repository at this point in the history
* WIP

* Rearrange routes

* Ability to resubmit

* Ability to upload images in resubmission

* Success screen

* Loading state

* Disable create new autocomplete abilities in resubmit (temp)

* tidy ups

* Remove unused import

* Fix imports

* Update SurveyResponsePage.tsx

* Fix tests

* Update packages/datatrak-web-server/src/routes/SingleSurveyResponseRoute.ts

Co-authored-by: Rohan Port <59544282+rohan-bes@users.noreply.github.com>

* feat(adminPanel): RN-1243: Resubmit survey response modal should link to Datatrak Web (#5640)

* WIP

* WIP

* WIP

* Styling

* WIP

* Error dismiss

* WIP

* Use updated entity country code if applicable

* Don't update user project/country if in resubmit mode

* Fix build

* Add REACT_APP_DATATRAK_WEB_URL to .env.example

* Build fixes

* Apply primary entity answer to resubmit

* Fix breaking data_time questions

* Fix build

* Fix survey responses with file uploads

* Display file name for saved file questions and fix remove file value

* Use dataTime for date questions

* fix permissions

* Send timezone through with resubmission

* Open up permissions

* feat(dataTrak): RN-1274: Keep 'outdated' historical survey responses when resubmitting (#5758)

* RN-1274: change route to `surveyResponses/:id/resubmit`

* RN-1274: Reworked ResubmitSurveyResponse route to create a new response and outdate the previous one

* RN-1274: Added resubmitSurveyResponse to the CentralApi

* RN-1274: Added ResubmitSurveyResponseRoute to datatrak-web-server

* RN-1274: Reworked survey response resubmission in datatrak to use the new backend routes

* Fix tests

* Edit survey response metadata

* Fix tests

* Edit survey response metadata

* Resubmit survey response with original data time and user ID

* PR fixes

* take 1

* feat(adminPanel): RN-1228: Link surveys to Datatrak Web (#5671)

* Make links

* Use projectId

* Update user preferences if project id is in url

* Add comment

* Allow country codes to be fetched for surveys

* Link directly to survey

* Default to DL and alphabetise the country codes

* Change tooltip text

* Update copy

* Hide button for surveys with no countries

---------

Co-authored-by: Andrew <vanbeekandrew@gmail.com>

* Fix dataTime timezone change

* Allow file questions to be viewed and changed

* feat(tupaiaWeb): RN-1367: Multiphotograph viz captions + restyle (#5769)

* Add `label` property to view data

* Preview display

* Display max 3

* WIP carousel

* WIP

* Working thumbnails

* Working carousel

* WiP

* Styling

* Add comments

* Update schemas.ts

* Adjust height and alignment

* Make images contained

* Fix responsive issue

---------

Co-authored-by: Andrew <vanbeekandrew@gmail.com>

* Fix issue

* Fix date of data

* Add outdated column to survey responses in admin panel

* Reset entity question values when filter questions change

* fix(datatrakWeb): Fix country code selector in reports export

* fix(adminPanel): RN-1375: update 'Add' project editor for consistency (#5816)

update editor column for consistency

* Handle existing file answers

* Use existing entity id if present

* Add pill styling for response status field

* Handle file names

* Change pill colours around

* Handle survey response file names

* Don't save file url in answer

* Fix tests

* Don't default dates on resubmit

* Handle when photo answer is a url

* Allow `null` default date for resubmission

* Save previous metadata on tracked entity

* Fix undefined models error

* Update project.pbxproj

* Hide survey resubmit button for outdated responses

* tweak(tupaiaWeb): RN-1394: Update tool tip for visualisation export (#5824)

Tool tip update

* tweak(adminPanel): RN-1399: Update icon and color as per Figma layouts (#5825)

Download Icon update

* tweak(adminPanel): RN-1274: Remove outdated survey responses and associated answers from DHIS via sync queue (#5827)

* Remove outdated survey responses and associated answers from dhis via sync queue

* Add tests

* Add answers back into queue when survey response is changed back to current

* Handle answers for outdated->current tests

* Fix tests

* Revert change to filter

* Ignore outdated surveys from exports

* Code question should be code generator type

* Fix tests

* Fix timezone issues

* Update processSurveyResponse.test.ts

* Get all answers for survey response

* Handle when photo includes a url

* Fix tests

* Fix crashing error

* Concert jpeg to jpg

* Keep existing survey response timezone

* fix(tupaiaWeb): RN-1414: Fix dashboard item permission error (#5836)

Update ReportPermissionsChecker.js

* Timezones

* fix(adminPanel): RN-1289: update the entity associated with a survey resubmission (#5817)

* Initial update

* test updates

* Update importSurveyResponses.js

* Update importSurveyResponses.js

* Update importSurveyResponses.js

* Update SurveyResponseUpdatePersistor.js

* Delete ~$nonPeriodicUpdates.xlsx

* test updates

* review comments

* review updates

* addition of tests

---------

Co-authored-by: Andrew <vanbeekandrew@gmail.com>

* Convert data_time to timezone date on server

* Fix tests

* Make dates/times zoneless so that they appear the same to everyone

* Fix tests

* Fix timezone offsets

* Handle timezones with DST

* Fixes

---------

Co-authored-by: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Co-authored-by: Salman <114740396+hrazasalman@users.noreply.github.com>
Co-authored-by: Andrew <vanbeekandrew@gmail.com>
Co-authored-by: Tom Caiger <caigertom@gmail.com>

---------

Co-authored-by: Rohan Port <59544282+rohan-bes@users.noreply.github.com>
Co-authored-by: Andrew <vanbeekandrew@gmail.com>
Co-authored-by: Salman <114740396+hrazasalman@users.noreply.github.com>
Co-authored-by: Tom Caiger <caigertom@gmail.com>
  • Loading branch information
5 people authored Aug 15, 2024
1 parent f7c9fec commit 5e707a1
Show file tree
Hide file tree
Showing 83 changed files with 1,755 additions and 738 deletions.
1 change: 1 addition & 0 deletions packages/admin-panel/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ REACT_APP_CLIENT_BASIC_AUTH_HEADER=
REACT_APP_VIZ_BUILDER_API_URL=
SKIP_PREFLIGHT_CHECK=
PARSE_LINK_HEADER_MAXLEN=
REACT_APP_DATATRAK_WEB_URL=
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const useEntities = search =>
['entities', search],
async () => {
const endpoint = stringifyQuery(undefined, `entities`, {
columns: JSON.stringify(['name', 'code', 'id']),
columns: JSON.stringify(['name', 'code', 'id', 'country_code']),
filter: JSON.stringify({
name: { comparator: 'ilike', comparisonValue: `%${search}%`, castAs: 'text' },
}),
Expand Down
26 changes: 26 additions & 0 deletions packages/admin-panel/src/api/mutations/useEditSurveyResponse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import { useMutation, useQueryClient } from 'react-query';
import { useApiContext } from '../../utilities/ApiProvider';

export const useEditSurveyResponse = (surveyResponseId, updatedSurveyResponse) => {
const queryClient = useQueryClient();
const api = useApiContext();
return useMutation(
[`surveyResponseEdit`, surveyResponseId, updatedSurveyResponse],
() => {
return api.put(`surveyResponses/${surveyResponseId}`, null, updatedSurveyResponse);
},
{
throwOnError: true,
onSuccess: async () => {
// invalidate the survey response data
await queryClient.invalidateQueries(['surveyResubmitData', surveyResponseId]);
return 'completed';
},
},
);
};

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,45 @@
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import { getBrowserTimeZone } from '@tupaia/utils';
import moment from 'moment';
import { ApprovalStatus } from '@tupaia/types';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { SurveyResponsesExportModal } from '../../importExport';
import { getPluralForm } from '../../pages/resources/resourceName';
import { OutdatedFilter } from '../../table/columnTypes/columnFilters';

const RESOURCE_NAME = { singular: 'survey response' };

// Don't include not_required as an editable option because it can lead to
// mis-matches between surveys and survey responses
export const APPROVAL_STATUS_TYPES = Object.values(ApprovalStatus).map(type => ({
label: type,
value: type,
}));
const GREEN = '#47CA80';
const GREY = '#898989';

const Pill = styled.span`
background-color: ${({ $color }) => {
return `${$color}33`; // slightly transparent
}};
border-radius: 1.5rem;
padding: 0.3rem 0.9rem;
color: ${({ $color }) => $color};
.cell-content:has(&) > div {
overflow: visible;
}
`;

const ResponseStatusPill = ({ value }) => {
const text = value ? 'Outdated' : 'Current';
const color = value ? GREY : GREEN;
return <Pill $color={color}>{text}</Pill>;
};

ResponseStatusPill.propTypes = {
value: PropTypes.bool,
};

ResponseStatusPill.defaultProps = {
value: false,
};

const surveyName = {
Header: 'Survey',
Expand Down Expand Up @@ -56,9 +80,13 @@ const dateOfData = {
},
};

const approvalStatus = {
Header: 'Approval status',
source: 'approval_status',
const responseStatus = {
Header: 'Response status',
source: 'outdated',
Filter: OutdatedFilter,
width: 180,
// eslint-disable-next-line react/prop-types
Cell: ({ value }) => <ResponseStatusPill value={value} />,
};

const entityName = {
Expand All @@ -80,7 +108,7 @@ export const SURVEY_RESPONSE_COLUMNS = [
assessorName,
date,
dateOfData,
approvalStatus,
responseStatus,
{
Header: 'Export',
type: 'export',
Expand Down
191 changes: 85 additions & 106 deletions packages/admin-panel/src/surveyResponse/Form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,38 @@

import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Button } from '@tupaia/ui-components';
import { Divider } from '@material-ui/core';
import { useGetExistingData } from './useGetExistingData';
import { ModalContentProvider, ModalFooter } from '../widgets';
import { useResubmitSurveyResponse } from '../api/mutations/useResubmitSurveyResponse';
import { MODAL_STATUS } from './constants';
import { SurveyScreens } from './SurveyScreens';
import { useEditSurveyResponse } from '../api/mutations/useEditSurveyResponse';
import { ResponseFields } from './ResponseFields';

const ButtonGroup = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
`;

export const Form = ({ surveyResponseId, onDismiss, onAfterMutate }) => {
const [surveyResubmission, setSurveyResubmission] = useState({});
const [filesByQuestionCode, setFilesByQuestionCode] = useState({});
const isUnchanged = Object.keys(surveyResubmission).length === 0;
const [resubmitStatus, setResubmitStatus] = useState(MODAL_STATUS.INITIAL);
const [editedData, setEditedData] = useState({});
const isUnchanged = Object.keys(editedData).length === 0;

const [selectedEntity, setSelectedEntity] = useState({});
const [resubmitError, setResubmitError] = useState(null);

const useResubmitResponse = () => {
// Swap filesByQuestionCode to filesByUniqueFileName.
// Tracking by question code allows us to manage files easier e.g. don't have to worry about tracking them in deletions
// And the API endpoint needs them by uniqueFileName
const filesByUniqueFileName = {};
for (const [questionCode, file] of Object.entries(filesByQuestionCode)) {
const uniqueFileName = surveyResubmission.answers[questionCode];
filesByUniqueFileName[uniqueFileName] = file;
}
return useResubmitSurveyResponse(surveyResponseId, surveyResubmission, filesByUniqueFileName);
};
const { mutateAsync: resubmitResponse } = useResubmitResponse();

const handleResubmit = useCallback(async () => {
setResubmitStatus(MODAL_STATUS.LOADING);
try {
await resubmitResponse();
} catch (e) {
setResubmitStatus(MODAL_STATUS.ERROR);
setResubmitError(e);
return;
}
setResubmitStatus(MODAL_STATUS.SUCCESS);
onAfterMutate();
});
const {
mutateAsync: editResponse,
isLoading,
isError,
error: editError,
reset, // reset the mutation state so we can dismiss the error
isSuccess,
} = useEditSurveyResponse(surveyResponseId, editedData);

const { data, isLoading: isFetching, error: fetchError } = useGetExistingData(surveyResponseId);

const existingAndNewFields = { ...data?.surveyResponse, ...editedData };

useEffect(() => {
if (!data) {
setSelectedEntity({});
Expand All @@ -59,88 +45,81 @@ export const Form = ({ surveyResponseId, onDismiss, onAfterMutate }) => {
}
}, [data]);

const handleDismissError = () => {
setResubmitStatus(MODAL_STATUS.INITIAL);
setResubmitError(null);
const resubmitSurveyResponse = async () => {
await editResponse();
onAfterMutate();
};

const onSetFormFile = (questionCode, file) => {
setFilesByQuestionCode({ ...filesByQuestionCode, [questionCode]: file });
const getDatatrakBaseUrl = () => {
if (import.meta.env.REACT_APP_DATATRAK_WEB_URL)
return import.meta.env.REACT_APP_DATATRAK_WEB_URL;
const { origin } = window.location;
if (origin.includes('localhost')) return 'https://dev-datatrak.tupaia.org';
return origin.replace('admin', 'datatrak');
};

const renderButtons = useCallback(() => {
switch (resubmitStatus) {
case MODAL_STATUS.LOADING:
return <></>;
case MODAL_STATUS.ERROR:
return (
<>
<Button variant="outlined" onClick={() => handleDismissError()}>
Dismiss
</Button>
</>
);
case MODAL_STATUS.SUCCESS:
return (
<>
<Button onClick={onDismiss}>Close</Button>
</>
);
case MODAL_STATUS.INITIAL:
default:
return (
<>
<Button variant="outlined" onClick={onDismiss}>
Cancel
</Button>
<Button
id="form-button-resubmit"
type="submit"
onClick={() => handleResubmit()}
disabled={isFetching || isUnchanged}
>
Resubmit
</Button>
</>
);
const resubmitResponseAndRedirect = async () => {
// If the response has been changed, resubmit it before redirecting
if (!isUnchanged) {
await editResponse();
onAfterMutate();
}
}, [resubmitStatus, isFetching, isUnchanged]);
const { country_code: updatedCountryCode } = selectedEntity;
const { survey, primaryEntity } = data;
const countryCodeToUse = updatedCountryCode || primaryEntity.country_code;
const datatrakBaseUrl = getDatatrakBaseUrl();
const url = `${datatrakBaseUrl}/survey/${countryCodeToUse}/${survey.code}/resubmit/${surveyResponseId}`;
// Open the URL in a new tab, so the user can resubmit the response in Datatrak
window.open(url, '_blank');
};

const existingAndNewFields = { ...data?.surveyResponse, ...surveyResubmission };
const isResubmitting = resubmitStatus === MODAL_STATUS.LOADING;
const isResubmitSuccess = resubmitStatus === MODAL_STATUS.SUCCESS;
const renderButtons = useCallback(() => {
if (isLoading) return null;
if (isError)
return (
<Button variant="outlined" onClick={reset}>
Dismiss
</Button>
);
if (isSuccess) return <Button onClick={onDismiss}>Close</Button>;
return (
<ButtonGroup>
<Button
id="form-button-resubmit"
onClick={resubmitSurveyResponse}
variant="outlined"
disabled={isFetching || isUnchanged}
color="primary"
>
Save and close
</Button>
<div>
<Button variant="outlined" onClick={onDismiss}>
Cancel
</Button>
<Button id="form-button-next" onClick={resubmitResponseAndRedirect} disabled={isFetching}>
Next
</Button>
</div>
</ButtonGroup>
);
}, [isFetching, isUnchanged, isLoading, isError, isSuccess]);

return (
<>
<ModalContentProvider
isLoading={isFetching || isResubmitting}
error={fetchError || resubmitError}
>
{!isFetching && !isResubmitSuccess && (
<>
<ResponseFields
selectedEntity={selectedEntity}
surveyName={data?.survey.name}
fields={existingAndNewFields}
onChange={(field, updatedField) =>
setSurveyResubmission({ ...surveyResubmission, [field]: updatedField })
}
setSelectedEntity={setSelectedEntity}
/>
<Divider />
<SurveyScreens
onChange={(field, updatedField) =>
setSurveyResubmission({ ...surveyResubmission, [field]: updatedField })
}
onSetFormFile={onSetFormFile}
survey={data?.survey}
existingAnswers={data?.answers}
selectedEntity={selectedEntity}
fields={existingAndNewFields}
/>
</>
<ModalContentProvider isLoading={isFetching || isLoading} error={fetchError || editError}>
{!isFetching && !isSuccess && (
<ResponseFields
selectedEntity={selectedEntity}
surveyName={data?.survey.name}
fields={existingAndNewFields}
onChange={(field, updatedField) =>
setEditedData({ ...editedData, [field]: updatedField })
}
setSelectedEntity={setSelectedEntity}
/>
)}
{isResubmitSuccess && 'The survey response has been successfully submitted.'}
{isSuccess && 'The survey response has been successfully submitted.'}
</ModalContentProvider>
<ModalFooter>{renderButtons()}</ModalFooter>
</>
Expand Down
Loading

0 comments on commit 5e707a1

Please sign in to comment.