Skip to content

Commit

Permalink
PRMDR-873 - Enable ARF upload via ARF bucket (#367)
Browse files Browse the repository at this point in the history
* [PRMDR-873] Add option to test panel to enter ARF workflow, remove LG from ARF selection page

* [PRMDR-873] allow searchPatient lambda to return non active patients to GP ADMIN, add an non active patient to mock pds service

* [PRMDR-873] some refactoring around mock pds

* [PRMDR-873] Add S3 upload ability to ARF journey

* [PRMDR-873] Change create doc ref to use ARF bucket for ARF

* [PRMDR-873] improve error handling at arf file select page

* [PRMDR-873] ensure env var in create doc ref lambda

* [PRMDR-873] refactor and fix issue around S3 file path

* [PRMDR-873] refactor and fix issue around S3 file path

* [PRMDR-873] Add unit test

* [PRMDR-873] Keep unused frontend code as comment

* [PRMDR-873] minor fix

* [PRMDR-873] Improve test coverage

* [PRMDR-873] improve coverage

* [PRMDR-873] address sonarcloud issue

* [PRMDR-873] improve coverage

* [PRMDR-873] improve coverage

* [PRMDR-873] Remove LG related codes from ARF upload screen

* [PRMDR-873] Address PR comment (s3 file location builder)

* remove unused import
  • Loading branch information
joefong-nhs authored May 21, 2024
1 parent 6e6905c commit e4d59e3
Show file tree
Hide file tree
Showing 18 changed files with 374 additions and 413 deletions.
377 changes: 118 additions & 259 deletions app/src/components/blocks/_arf/selectStage/SelectStage.test.tsx

Large diffs are not rendered by default.

215 changes: 128 additions & 87 deletions app/src/components/blocks/_arf/selectStage/SelectStage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Dispatch, SetStateAction, useRef, useState } from 'react';
import React, { Dispatch, SetStateAction, useRef } from 'react';
import {
DOCUMENT_TYPE,
DOCUMENT_UPLOAD_STATE,
Expand All @@ -12,12 +12,19 @@ import { useController, useForm } from 'react-hook-form';
import toFileList from '../../../../helpers/utils/toFileList';
import PatientSummary from '../../../generic/patientSummary/PatientSummary';
import DocumentInputForm from '../documentInputForm/DocumentInputForm';
import { ARFFormConfig, lloydGeorgeFormConfig } from '../../../../helpers/utils/formConfig';
import { ARFFormConfig } from '../../../../helpers/utils/formConfig';
import { v4 as uuidv4 } from 'uuid';
import uploadDocuments from '../../../../helpers/requests/uploadDocuments';
import uploadDocuments, { uploadDocumentToS3 } from '../../../../helpers/requests/uploadDocuments';
import usePatient from '../../../../helpers/hooks/usePatient';
import useBaseAPIUrl from '../../../../helpers/hooks/useBaseAPIUrl';
import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders';
import BackButton from '../../../generic/backButton/BackButton';
import { UploadSession } from '../../../../types/generic/uploadResult';
import { AxiosError } from 'axios';
import { routes } from '../../../../types/generic/routes';
import { errorToParams } from '../../../../helpers/utils/errorToParams';
import { isMock } from '../../../../helpers/utils/isLocal';
import { useNavigate } from 'react-router';

interface Props {
setDocuments: SetUploadDocuments;
Expand All @@ -26,123 +33,157 @@ interface Props {
}

function SelectStage({ setDocuments, setStage, documents }: Props) {
const [arfDocuments, setArfDocuments] = useState<Array<UploadDocument>>([]);
const [lgDocuments, setLgDocuments] = useState<Array<UploadDocument>>([]);
const baseUrl = useBaseAPIUrl();
const baseHeaders = useBaseAPIHeaders();
let arfInputRef = useRef<HTMLInputElement | null>(null);
let lgInputRef = useRef<HTMLInputElement | null>(null);
const navigate = useNavigate();
const arfInputRef = useRef<HTMLInputElement | null>(null);
const patientDetails = usePatient();
const nhsNumber: string = patientDetails?.nhsNumber ?? '';
const mergedDocuments = [...arfDocuments, ...lgDocuments];
const hasFileInput = mergedDocuments.length;
const hasFileInput = documents.length > 0;

const { handleSubmit, control, formState } = useForm();
const { handleSubmit, control, formState, setError } = useForm();

const lgController = useController(lloydGeorgeFormConfig(control));
const arfController = useController(ARFFormConfig(control));

const submitDocuments = async () => {
if (!hasFileInput) {
setError('arf-documents', { type: 'custom', message: 'Select a file to upload' });
return;
}

setStage(UPLOAD_STAGE.Uploading);
try {
await uploadDocuments({
const uploadSession = await uploadDocuments({
nhsNumber,
documents,
baseUrl,
baseHeaders,
});
} catch (e) {}
setStage(UPLOAD_STAGE.Complete);
const uploadingDocuments: UploadDocument[] =
addMetadataAndMarkDocumentAsUploading(uploadSession);
setDocuments(uploadingDocuments);

await uploadAllDocumentsToS3(uploadingDocuments, uploadSession);

setStage(UPLOAD_STAGE.Complete);
} catch (error) {
handleUploadError(error as AxiosError);
}
};

const markDocumentAsFailed = (failedDocument: UploadDocument) => {
setDocuments((prevState) =>
prevState.map((prevStateDocument) => {
if (prevStateDocument.id !== failedDocument.id) {
return prevStateDocument;
}
return { ...prevStateDocument, state: DOCUMENT_UPLOAD_STATE.FAILED, progress: 0 };
}),
);
};

const uploadAllDocumentsToS3 = (
uploadingDocuments: UploadDocument[],
uploadSession: UploadSession,
) => {
const allUploadPromises = uploadingDocuments.map((document) =>
uploadDocumentToS3({ setDocuments, document, uploadSession }).catch(() =>
markDocumentAsFailed(document),
),
);
return Promise.all(allUploadPromises);
};

const handleUploadError = (error: AxiosError) => {
if (error.response?.status === 403) {
navigate(routes.SESSION_EXPIRED);
} else if (isMock(error)) {
/* istanbul ignore next */
setDocuments((prevState) =>
prevState.map((doc) => ({
...doc,
state: DOCUMENT_UPLOAD_STATE.SUCCEEDED,
})),
);
/* istanbul ignore next */
setStage(UPLOAD_STAGE.Complete);
} else {
navigate(routes.SERVER_ERROR + errorToParams(error));
}
};

const addMetadataAndMarkDocumentAsUploading = (uploadSession: UploadSession) => {
return documents.map((doc) => {
const documentMetadata = uploadSession[doc.file.name];
const documentReference = documentMetadata.fields.key;
return {
...doc,
state: DOCUMENT_UPLOAD_STATE.UPLOADING,
key: documentReference,
ref: documentReference.split('/').at(-1),
};
});
};

const onInput = (e: FileInputEvent, docType: DOCUMENT_TYPE) => {
const fileArray = Array.from(e.target.files ?? new FileList());
const documentMap: Array<UploadDocument> = fileArray.map((file) => ({
const newlyAddedDocuments: Array<UploadDocument> = fileArray.map((file) => ({
id: uuidv4(),
file,
state: DOCUMENT_UPLOAD_STATE.SELECTED,
progress: 0,
docType: docType,
attempts: 0,
}));
const isArfDoc = docType === DOCUMENT_TYPE.ARF;
const mergeList = isArfDoc ? lgDocuments : arfDocuments;
const docTypeList = isArfDoc ? arfDocuments : lgDocuments;
const updatedDocList = [...documentMap, ...docTypeList];
if (isArfDoc) {
setArfDocuments(updatedDocList);
arfController.field.onChange(updatedDocList);
} else {
setLgDocuments(updatedDocList);
lgController.field.onChange(updatedDocList);
}
const updatedFileList = [...mergeList, ...updatedDocList];
setDocuments(updatedFileList);
const updatedDocList = [...newlyAddedDocuments, ...documents];
arfController.field.onChange(updatedDocList);
setDocuments(updatedDocList);
};

const onRemove = (index: number, docType: DOCUMENT_TYPE) => {
const isArfDoc = docType === DOCUMENT_TYPE.ARF;
const mergeList = isArfDoc ? lgDocuments : arfDocuments;
const docTypeList = isArfDoc ? arfDocuments : lgDocuments;
const updatedDocList = [...docTypeList.slice(0, index), ...docTypeList.slice(index + 1)];
if (isArfDoc) {
setArfDocuments(updatedDocList);
if (arfInputRef.current) {
arfInputRef.current.files = toFileList(updatedDocList);
arfController.field.onChange(updatedDocList);
}
} else {
setLgDocuments(updatedDocList);
if (lgInputRef.current) {
lgInputRef.current.files = toFileList(updatedDocList);
lgController.field.onChange(updatedDocList);
}
const onRemove = (index: number, _docType: DOCUMENT_TYPE) => {
const updatedDocList: UploadDocument[] = [
...documents.slice(0, index),
...documents.slice(index + 1),
];

if (arfInputRef.current) {
arfInputRef.current.files = toFileList(updatedDocList);
arfController.field.onChange(updatedDocList);
}

const updatedFileList = [...mergeList, ...updatedDocList];
setDocuments(updatedFileList);
setDocuments(updatedDocList);
};

return (
<form
onSubmit={handleSubmit(submitDocuments)}
noValidate
data-testid="upload-document-form"
>
<Fieldset.Legend headingLevel="h1" isPageHeading>
Upload documents
</Fieldset.Legend>
<PatientSummary />

<Fieldset>
<h2>Electronic health records</h2>
<DocumentInputForm
showHelp
documents={arfDocuments}
onDocumentRemove={onRemove}
onDocumentInput={onInput}
formController={arfController}
inputRef={arfInputRef}
formType={DOCUMENT_TYPE.ARF}
/>
</Fieldset>
<Fieldset>
<h2>Lloyd George records</h2>
<DocumentInputForm
documents={lgDocuments}
onDocumentRemove={onRemove}
onDocumentInput={onInput}
formController={lgController}
inputRef={lgInputRef}
formType={DOCUMENT_TYPE.LLOYD_GEORGE}
/>
</Fieldset>
<Button
type="submit"
id="upload-button"
disabled={formState.isSubmitting || !hasFileInput}
<>
<BackButton />
<form
onSubmit={handleSubmit(submitDocuments)}
noValidate
data-testid="upload-document-form"
>
Upload
</Button>
</form>
<Fieldset.Legend headingLevel="h1" isPageHeading>
Upload documents
</Fieldset.Legend>
<PatientSummary />

<Fieldset>
<h2>Electronic health records</h2>
<DocumentInputForm
showHelp
documents={documents}
onDocumentRemove={onRemove}
onDocumentInput={onInput}
formController={arfController}
inputRef={arfInputRef}
formType={DOCUMENT_TYPE.ARF}
/>
</Fieldset>
<Button type="submit" id="upload-button" disabled={formState.isSubmitting}>
Upload
</Button>
</form>
</>
);
}

Expand Down
9 changes: 8 additions & 1 deletion app/src/components/blocks/testPanel/TestPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import TestToggle, { ToggleProps } from './TestToggle';

function TestPanel() {
const [config, setConfig] = useConfigContext();
const { isBsol, recordUploaded, userRole } = config.mockLocal;
const { isBsol, recordUploaded, userRole, patientIsActive } = config.mockLocal;

const updateLocalFlag = (key: keyof LocalFlags, value: boolean | REPOSITORY_ROLE) => {
setConfig({
Expand Down Expand Up @@ -58,6 +58,13 @@ function TestPanel() {
updateLocalFlag('recordUploaded', !recordUploaded);
},
},
'patient-active-toggle': {
label: 'Patient is active (turn off to visit ARF workflow)',
checked: !!patientIsActive,
onChange: () => {
updateLocalFlag('patientIsActive', !patientIsActive);
},
},
};

return (
Expand Down
9 changes: 6 additions & 3 deletions app/src/pages/patientSearchPage/PatientSearchPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import axios from 'axios';
import { routes } from '../../types/generic/routes';
import { REPOSITORY_ROLE, authorisedRoles } from '../../types/generic/authRole';
import useRole from '../../helpers/hooks/useRole';
import ConfigProvider from '../../providers/configProvider/ConfigProvider';
import { runAxeTest } from '../../helpers/test/axeTestHelper';

const mockedUseNavigate = jest.fn();
Expand Down Expand Up @@ -312,8 +313,10 @@ describe('PatientSearchPage', () => {
const renderPatientSearchPage = () => {
const patient: PatientDetails = buildPatientDetails();
render(
<PatientDetailsProvider patientDetails={patient}>
<PatientSearchPage />
</PatientDetailsProvider>,
<ConfigProvider>
<PatientDetailsProvider patientDetails={patient}>
<PatientSearchPage />
</PatientDetailsProvider>
</ConfigProvider>,
);
};
7 changes: 6 additions & 1 deletion app/src/pages/patientSearchPage/PatientSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import useBaseAPIHeaders from '../../helpers/hooks/useBaseAPIHeaders';
import useBaseAPIUrl from '../../helpers/hooks/useBaseAPIUrl';
import { errorToParams } from '../../helpers/utils/errorToParams';
import useTitle from '../../helpers/hooks/useTitle';
import useConfig from '../../helpers/hooks/useConfig';

export const incorrectFormatMessage = "Enter patient's 10 digit NHS number";

Expand All @@ -27,6 +28,7 @@ function PatientSearchPage() {
const [submissionState, setSubmissionState] = useState<SEARCH_STATES>(SEARCH_STATES.IDLE);
const [statusCode, setStatusCode] = useState<null | number>(null);
const [inputError, setInputError] = useState<null | string>(null);
const { mockLocal } = useConfig();
const { register, handleSubmit } = useForm({
reValidateMode: 'onSubmit',
});
Expand Down Expand Up @@ -64,8 +66,11 @@ function PatientSearchPage() {
handleSuccess(patientDetails);
} catch (e) {
const error = e as AxiosError;
/* istanbul ignore if */
if (isMock(error)) {
handleSuccess(buildPatientDetails({ nhsNumber }));
handleSuccess(
buildPatientDetails({ nhsNumber, active: mockLocal.patientIsActive }),
);
return;
}
if (error.response?.status === 400) {
Expand Down
2 changes: 2 additions & 0 deletions app/src/providers/configProvider/ConfigProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type LocalFlags = {
isBsol?: boolean;
recordUploaded?: boolean;
userRole?: REPOSITORY_ROLE;
patientIsActive?: boolean;
};

export type GlobalConfig = {
Expand Down Expand Up @@ -43,6 +44,7 @@ const ConfigProvider = ({ children, configOverride, setConfigOverride }: Props)
? {
isBsol: true,
recordUploaded: true,
patientIsActive: true,
userRole: REPOSITORY_ROLE.GP_ADMIN,
}
: null;
Expand Down
1 change: 1 addition & 0 deletions lambdas/handlers/create_document_reference_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"DOCUMENT_STORE_DYNAMODB_NAME",
"LLOYD_GEORGE_DYNAMODB_NAME",
"STAGING_STORE_BUCKET_NAME",
"DOCUMENT_STORE_BUCKET_NAME",
]
)
@override_error_check
Expand Down
Loading

0 comments on commit e4d59e3

Please sign in to comment.