diff --git a/data-serving/data-service/api/openapi.yaml b/data-serving/data-service/api/openapi.yaml
index 9354fe81..27af56ac 100644
--- a/data-serving/data-service/api/openapi.yaml
+++ b/data-serving/data-service/api/openapi.yaml
@@ -684,6 +684,8 @@ components:
- suspected
- discarded
- omit_error
+ comment:
+ type: string
caseReference:
type: object
properties:
diff --git a/data-serving/data-service/src/controllers/case.ts b/data-serving/data-service/src/controllers/case.ts
index aff67c59..43165884 100644
--- a/data-serving/data-service/src/controllers/case.ts
+++ b/data-serving/data-service/src/controllers/case.ts
@@ -316,6 +316,7 @@ export class CasesController {
while (doc != null) {
delete doc.caseReference.sourceEntryId;
const caseDTO = await dtoFromCase(doc);
+ delete caseDTO.comment;
const stringifiedCase = stringify([caseDTO], {
header: false,
columns: this.csvHeaders,
diff --git a/data-serving/data-service/src/model/day0-case.ts b/data-serving/data-service/src/model/day0-case.ts
index 8f204cfd..0b995120 100644
--- a/data-serving/data-service/src/model/day0-case.ts
+++ b/data-serving/data-service/src/model/day0-case.ts
@@ -80,6 +80,7 @@ export const caseSchema = new mongoose.Schema(
enum: CaseStatus,
required: true,
},
+ comment: String,
pathogen: {
type: String,
required: true,
@@ -149,6 +150,7 @@ caseSchema.methods.equalsJSON = function (jsonCase: any): boolean {
return (
_.isEqual(thisJson.caseStatus, other.caseStatus) &&
+ _.isEqual(thisJson.comment, other.comment) &&
_.isEqual(thisJson.demographics, other.demographics) &&
_.isEqual(thisJson.events, other.events) &&
_.isEqual(thisJson.genomeSequences, other.genomeSequences) &&
@@ -179,6 +181,7 @@ export interface ISource {
export type ICase = {
caseStatus: CaseStatus;
+ comment?: string;
pathogen: string;
symptoms: string;
dateLastModified: string;
diff --git a/data-serving/data-service/src/model/fields.json b/data-serving/data-service/src/model/fields.json
index b15f4507..04a1d690 100644
--- a/data-serving/data-service/src/model/fields.json
+++ b/data-serving/data-service/src/model/fields.json
@@ -1,6 +1,7 @@
[
"_id",
"caseStatus",
+ "comment",
"pathogen",
"caseReference.additionalSources",
"caseReference.sourceEntryId",
diff --git a/verification/curator-service/api/openapi/openapi.yaml b/verification/curator-service/api/openapi/openapi.yaml
index d43a5eaf..145e9ae0 100644
--- a/verification/curator-service/api/openapi/openapi.yaml
+++ b/verification/curator-service/api/openapi/openapi.yaml
@@ -1423,6 +1423,8 @@ components:
- suspected
- discarded
- omit_error
+ comment:
+ type: string
caseReference:
type: object
properties:
diff --git a/verification/curator-service/ui/cypress/e2e/components/Curator.spec.ts b/verification/curator-service/ui/cypress/e2e/components/Curator.spec.ts
index 860d0720..2adf9adc 100644
--- a/verification/curator-service/ui/cypress/e2e/components/Curator.spec.ts
+++ b/verification/curator-service/ui/cypress/e2e/components/Curator.spec.ts
@@ -56,6 +56,7 @@ describe('Curator', function () {
// GENERAL
cy.get('div[data-testid="caseStatus"]').click();
cy.get('li[data-value="confirmed"').click();
+ cy.get('div[data-testid="comment"]').type('This case should be consulted with Supervisor.');
// DATA SOURCE
@@ -426,6 +427,8 @@ describe('Curator', function () {
// TODO UI for demographics.age needs redesigning
// View full details about the case
cy.contains('td', 'www.example.com').click({ force: true });
+ // Curator's comment.
+ cy.contains('This case should be consulted with Supervisor.');
// Case data.
cy.contains('www.example.com');
// Demographics.
diff --git a/verification/curator-service/ui/src/api/models/Day0Case.ts b/verification/curator-service/ui/src/api/models/Day0Case.ts
index db8d6a72..bf4271e3 100644
--- a/verification/curator-service/ui/src/api/models/Day0Case.ts
+++ b/verification/curator-service/ui/src/api/models/Day0Case.ts
@@ -174,6 +174,7 @@ export interface Curators {
export interface Day0Case {
_id?: string;
caseStatus: CaseStatus | '';
+ comment?: string;
caseReference: CaseReference;
demographics: Demographics;
location: Location;
@@ -224,6 +225,7 @@ interface VaccinationFormValues {
// contains all the fields present in manual case entry form
export interface Day0CaseFormValues {
caseStatus: CaseStatus | '';
+ comment?: string;
caseReference: {
sourceId: string;
sourceUrl: string;
diff --git a/verification/curator-service/ui/src/components/BulkCaseForm.tsx b/verification/curator-service/ui/src/components/BulkCaseForm.tsx
index dce95a46..d9ec0e51 100644
--- a/verification/curator-service/ui/src/components/BulkCaseForm.tsx
+++ b/verification/curator-service/ui/src/components/BulkCaseForm.tsx
@@ -101,6 +101,7 @@ interface RawParsedCase {
[key: string]: string | number | boolean | undefined;
caseStatus: CaseStatus;
+ comment?: string;
pathogen: string;
// CaseReference
@@ -255,6 +256,7 @@ const BulkCaseForm = (props: BulkCaseFormProps) => {
): CompleteParsedCase => {
return {
caseStatus: c.caseStatus,
+ comment: c.comment,
pathogen: c.pathogen,
caseReference: {
sourceId: caseReference.sourceId,
diff --git a/verification/curator-service/ui/src/components/CaseForm.tsx b/verification/curator-service/ui/src/components/CaseForm.tsx
index 5fab61c2..1db8fa33 100644
--- a/verification/curator-service/ui/src/components/CaseForm.tsx
+++ b/verification/curator-service/ui/src/components/CaseForm.tsx
@@ -65,6 +65,7 @@ const initialValuesFromCase = (
// return minimal viable case
return {
caseStatus: '',
+ comment: '',
caseReference: {
sourceId: '',
sourceUrl: '',
@@ -255,6 +256,7 @@ const NewCaseValidation = Yup.object().shape(
caseStatus: Yup.string()
.oneOf(['confirmed', 'suspected', 'discarded', 'omit_error'])
.required('Required'),
+ comment: Yup.string(),
caseReference: Yup.object().shape({
sourceUrl: Yup.string()
.required('Required')
@@ -670,65 +672,64 @@ export default function CaseForm(props: Props): JSX.Element {
- scrollTo('demographics')
+ scrollTo('location')
}
>
{tableOfContentsIcon({
isChecked: isChecked({
- optionalValues: [
- values.demographics.gender,
- values.demographics.age,
- values.demographics
- .occupation,
- values.demographics
- .healthcareWorker,
+ requiredValues: [
+ values.location.countryISO3,
],
}),
hasError: hasErrors(
- ['demographics'],
+ ['location'],
errors,
touched,
),
})}
- {'Demographics'.toLocaleUpperCase()}
+ {'Location'.toLocaleUpperCase()}
-
- scrollTo('location')
- }
+ onClick={(): void => scrollTo('events')}
>
{tableOfContentsIcon({
isChecked: isChecked({
requiredValues: [
- values.location.countryISO3,
+ values.events.dateEntry,
+ values.events.dateReported,
],
}),
hasError: hasErrors(
- ['location'],
+ ['events'],
errors,
touched,
),
})}
- {'Location'.toLocaleUpperCase()}
+ {'Events'.toLocaleUpperCase()}
scrollTo('events')}
+ onClick={(): void =>
+ scrollTo('demographics')
+ }
>
{tableOfContentsIcon({
isChecked: isChecked({
- requiredValues: [
- values.events.dateEntry,
- values.events.dateReported,
+ optionalValues: [
+ values.demographics.gender,
+ values.demographics.age,
+ values.demographics
+ .occupation,
+ values.demographics
+ .healthcareWorker,
],
}),
hasError: hasErrors(
- ['events'],
+ ['demographics'],
errors,
touched,
),
})}
- {'Events'.toLocaleUpperCase()}
+ {'Demographics'.toLocaleUpperCase()}
@@ -949,15 +950,15 @@ export default function CaseForm(props: Props): JSX.Element {
hasSourceEntryId
/>
-
-
-
+
+
+
diff --git a/verification/curator-service/ui/src/components/LinelistTable/helperFunctions.ts b/verification/curator-service/ui/src/components/LinelistTable/helperFunctions.ts
index cb437c79..171e039b 100644
--- a/verification/curator-service/ui/src/components/LinelistTable/helperFunctions.ts
+++ b/verification/curator-service/ui/src/components/LinelistTable/helperFunctions.ts
@@ -32,12 +32,14 @@ export const createData = (
dateOnset?: string,
source?: string,
caseStatus?: string,
+ comment?: string,
) => {
return {
caseId: caseId || '',
dateEntry: dateEntry || '',
dateReported: dateReported || '',
caseStatus: caseStatus || '',
+ comment: comment || '',
country: country || '',
region: region || '',
district: district || '',
diff --git a/verification/curator-service/ui/src/components/LinelistTable/index.tsx b/verification/curator-service/ui/src/components/LinelistTable/index.tsx
index 5c95959b..d93d24dd 100644
--- a/verification/curator-service/ui/src/components/LinelistTable/index.tsx
+++ b/verification/curator-service/ui/src/components/LinelistTable/index.tsx
@@ -128,6 +128,7 @@ const LinelistTable = () => {
renderDate(data.events.dateOnset) || '-',
data.caseReference.sourceUrl || '-',
data.caseStatus || '-',
+ data.comment || '',
);
});
diff --git a/verification/curator-service/ui/src/components/ViewCase.tsx b/verification/curator-service/ui/src/components/ViewCase.tsx
index eee68460..d59e7370 100644
--- a/verification/curator-service/ui/src/components/ViewCase.tsx
+++ b/verification/curator-service/ui/src/components/ViewCase.tsx
@@ -225,23 +225,23 @@ function CaseDetails(props: CaseDetailsProps): JSX.Element {
+
+
- {/* DEMOGRAPHICS */}
-
-
-
- Demographics
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{/* LOCATION */}
@@ -708,6 +677,39 @@ function CaseDetails(props: CaseDetailsProps): JSX.Element {
+ {/* DEMOGRAPHICS */}
+
+
+
+ Demographics
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{/* SYMPTOMS */}
@@ -951,6 +953,7 @@ function RowHeader(props: { title: string }): JSX.Element {
function RowContent(props: {
content?: string;
isLink?: boolean;
+ isMultiline?: boolean;
}): JSX.Element {
const searchQuery = useSelector(selectSearchQuery);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -984,6 +987,7 @@ function RowContent(props: {
) : (
(
Entry date: Date case was entered into line
list.
+
+ Curator's comment: Comment added by curator,
+ visible only in turnkey system, not downloadable.
+
);
@@ -49,6 +53,17 @@ export default function General(): JSX.Element {
fullWidth
/>
+
+
+
);
}