Skip to content

Commit

Permalink
feat: [DHIS-11419] display assigned users on events in enrollment ove…
Browse files Browse the repository at this point in the history
…rview page (#3453)
  • Loading branch information
superskip authored Dec 18, 2023
1 parent 8abdca3 commit 44fb2be
Show file tree
Hide file tree
Showing 14 changed files with 188 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,28 +1,99 @@
// @flow
import { useEffect } from 'react';
import { useDataQuery } from '@dhis2/app-runtime';

const query = {
programData: {
resource: 'programs',
id: ({ id }) => id,
params: {
fields:
['programStages[id,repeatable,hideDueDate,programStageDataElements[displayInReports,dataElement[id,valueType,displayName,displayFormName,optionSet[options[code,name]]]'],
},
},
};
import { useMemo } from 'react';
import { useProgramFromIndexedDB } from '../../../../../utils/cachedDataHooks/useProgramFromIndexedDB';
import { useDataElementsFromIndexedDB } from '../../../../../utils/cachedDataHooks/useDataElementsFromIndexedDB';
import { useOptionSetsFromIndexedDB } from '../../../../../utils/cachedDataHooks/useOptionSetsFromIndexedDB';

const queryKey = 'useProgramMetadata';

export const useProgramMetadata = (programId: string) => {
const { data, error, loading, refetch } = useDataQuery(query, {
lazy: true,
});
const { program, isLoading, isError } = useProgramFromIndexedDB(programId, { enabled: !!programId });

const dataElementIds = useMemo(() =>
(program ? program.programStages.reduce(
(acc, stage) => stage.programStageDataElements.reduce(
(accIds, dataElement) => {
accIds.add(dataElement.dataElementId);
return accIds;
}, acc),
new Set) : undefined), [program]);

const {
isLoading: loadingDataElements,
dataElements,
isError: dataElementsError,
} = useDataElementsFromIndexedDB([queryKey, programId], dataElementIds);

const derivedDataElementValues = useMemo(() =>
(dataElements ? ({
optionSetIds: dataElements.reduce((acc, dataElement) => {
if (dataElement.optionSetValue) {
acc.add(dataElement.optionSet.id);
}
return acc;
}, new Set),
dataElementDictionary: dataElements.reduce((acc, dataElement) => {
acc[dataElement.id] = dataElement;
return acc;
}, {}),
}) : undefined), [dataElements]);

useEffect(() => {
if (programId) {
refetch({ id: programId });
const {
isLoading: loadingOptionSets,
optionSets,
isError: optionSetsError,
} = useOptionSetsFromIndexedDB([queryKey, programId], derivedDataElementValues && derivedDataElementValues.optionSetIds);

const optionSetDictionary = useMemo(
() => (optionSets ? optionSets.reduce(
(acc, optionSet) => {
acc[optionSet.id] = {
optionSet: {
options: optionSet.options.map(option => ({
name: option.displayName,
code: option.code,
})),
},
};
return acc;
}, {}) : undefined),
[optionSets],
);

const programMetadata = useMemo(() => {
if (!program || !derivedDataElementValues || !optionSetDictionary) {
return undefined;
}
}, [refetch, programId]);

return { error, programMetadata: !loading && data?.programData ? data.programData : undefined };
const dataElementDictionary = derivedDataElementValues.dataElementDictionary;

return {
programStages: program.programStages.map(stage => ({
id: stage.id,
repeatable: stage.repeatable,
hideDueDate: stage.hideDueDate,
enableUserAssignment: stage.enableUserAssignment,
programStageDataElements: stage.programStageDataElements
.map((programStageDataElement) => {
const dataElement = dataElementDictionary[programStageDataElement.dataElementId];
return {
displayInReports: programStageDataElement.displayInReports,
dataElement: {
id: dataElement.id,
valueType: dataElement.valueType,
displayName: dataElement.displayName,
displayFormName: dataElement.displayFormName,
optionSet: dataElement.optionSetValue ? optionSetDictionary[dataElement.optionSet.id] : {},
},
};
}),
})),
};
}, [program, derivedDataElementValues, optionSetDictionary]);


return {
error: (isError || dataElementsError || optionSetsError) && { programId },
programMetadata: (isLoading || loadingDataElements || loadingOptionSets) ? undefined : programMetadata,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const useProgramStages = (program: Program, programStages?: Array<apiProg
if (program && programStages) {
program.stages.forEach((item) => {
const { id, name, icon, stageForm } = item;
const { hideDueDate, programStageDataElements, repeatable } = programStages.find(p => p.id === id) || {};
const { hideDueDate, programStageDataElements, repeatable, enableUserAssignment } = programStages.find(p => p.id === id) || {};
if (!programStageDataElements) {
log.error(errorCreator(i18n.t('Program stage not found'))(id));
} else {
Expand All @@ -21,6 +21,7 @@ export const useProgramStages = (program: Program, programStages?: Array<apiProg
icon,
hideDueDate,
repeatable,
enableUserAssignment,
description: stageForm.description,
dataElements: programStageDataElements?.reduce((acc, currentStageData) => {
const { displayInReports, dataElement } = currentStageData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const hideProgramStage = (ruleEffects, stageId) => (

export const StagePlain = ({ stage, events, classes, className, onCreateNew, ruleEffects, ...passOnProps }: Props) => {
const [open, setOpenStatus] = useState(true);
const { id, name, icon, description, dataElements, hideDueDate, repeatable } = stage;
const { id, name, icon, description, dataElements, hideDueDate, repeatable, enableUserAssignment } = stage;
const hiddenProgramStage = hideProgramStage(ruleEffects, id);

return (
Expand All @@ -57,6 +57,7 @@ export const StagePlain = ({ stage, events, classes, className, onCreateNew, rul
dataElements={dataElements}
hideDueDate={hideDueDate}
repeatable={repeatable}
enableUserAssignment={enableUserAssignment}
onCreateNew={onCreateNew}
hiddenProgramStage={hiddenProgramStage}
{...passOnProps}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const StageDetailPlain = (props: Props) => {
dataElements,
hideDueDate = false,
repeatable = false,
enableUserAssignment = false,
onEventClick,
onViewAll,
onCreateNew,
Expand All @@ -76,7 +77,7 @@ const StageDetailPlain = (props: Props) => {
sortDirection: SORT_DIRECTION.DESC,
};
const { stage } = getProgramAndStageForProgram(programId, stageId);
const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, stage?.stageForm);
const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, enableUserAssignment, stage?.stageForm);
const { loading, value: dataSource, error } = useComputeDataFromEvent(dataElements, events);


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@ import { SORT_DIRECTION, MULIT_TEXT_WITH_NO_OPTIONS_SET } from './constants';
import { isNotValidOptionSet } from '../../../../../../utils/isNotValidOptionSet';
import { useOrgUnitNames } from '../../../../../../metadataRetrieval/orgUnitName';

const baseKeys = [{ id: 'status' }, { id: 'occurredAt' }, { id: 'orgUnitName' }, { id: 'scheduledAt' }, { id: 'comments' }];
const baseKeys = [{ id: 'status' }, { id: 'occurredAt' }, { id: 'assignedUser' }, { id: 'orgUnitName' }, { id: 'scheduledAt' }, { id: 'comments' }];
const basedFieldTypes = [
{ type: dataElementTypes.STATUS, resolveValue: convertStatusForView },
{ type: dataElementTypes.DATE },
{ type: 'ASSIGNEE' },
{ type: dataElementTypes.TEXT, resolveValue: convertOrgUnitForView },
{ type: dataElementTypes.DATE },
{ type: dataElementTypes.UNKNOWN, resolveValue: convertCommentForView },
];
const getBaseColumnHeaders = props => [
{ header: i18n.t('Status'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true },
{ header: props.formFoundation.getLabel('occurredAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true },
{ header: i18n.t('Assigned to'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true },
{ header: i18n.t('Registering unit'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true },
{ header: props.formFoundation.getLabel('scheduledAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true },
{ header: '', sortDirection: null, isPredefined: true },
Expand Down Expand Up @@ -118,7 +120,7 @@ const useComputeDataFromEvent = (dataElements: Array<StageDataElement>, events:
};


const useComputeHeaderColumn = (dataElements: Array<StageDataElement>, hideDueDate: boolean, formFoundation: Object) => {
const useComputeHeaderColumn = (dataElements: Array<StageDataElement>, hideDueDate: boolean, enableUserAssignment: boolean, formFoundation: Object) => {
const headerColumns = useMemo(() => {
const dataElementHeaders = dataElements.reduce((acc, currDataElement) => {
const { id, name, formName, type, optionSet } = currDataElement;
Expand All @@ -132,9 +134,10 @@ const useComputeHeaderColumn = (dataElements: Array<StageDataElement>, hideDueDa
return acc;
}, []);
return [
...getBaseColumns({ formFoundation }).filter(col => (hideDueDate ? col.id !== 'scheduledAt' : true)),
...getBaseColumns({ formFoundation })
.filter(col => (enableUserAssignment || col.id !== 'assignedUser') && (!hideDueDate || col.id !== 'scheduledAt')),
...dataElementHeaders];
}, [dataElements, hideDueDate, formFoundation]);
}, [dataElements, hideDueDate, enableUserAssignment, formFoundation]);

return headerColumns;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { StageDataElement, StageCommonProps } from '../../../types/common.t
eventName: string,
hideDueDate?: boolean,
repeatable?: boolean,
enableUserAssignment?: boolean,
stageId: string,
hiddenProgramStage?: boolean,
...CssClasses,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type Stage = {
description?: ?string,
icon?: Icon,
dataElements: Array<StageDataElement>,
enableUserAssignment: boolean,
hideDueDate?: boolean,
repeatable?: boolean
}
Expand Down
10 changes: 10 additions & 0 deletions src/core_modules/capture-core/converters/serverToClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ function convertTime(d2Value: string) {
return parseData.momentTime;
}

const convertAssignedUserToClient = (assignedUser?: ApiAssignedUser) =>
((assignedUser && assignedUser.uid) ? {
id: assignedUser.uid,
name: assignedUser.displayName,
username: assignedUser.username,
firstName: assignedUser.firstName,
surname: assignedUser.surname,
} : null);

const optionSetConvertersForType = {
[dataElementTypes.NUMBER]: parseNumber,
[dataElementTypes.INTEGER]: parseNumber,
Expand Down Expand Up @@ -51,6 +60,7 @@ const valueConvertersForType = {
return { latitude: arr[1], longitude: arr[0] };
},
[dataElementTypes.POLYGON]: () => 'Polygon',
[dataElementTypes.ASSIGNEE]: convertAssignedUserToClient,
};

export function convertValue(value: any, type: $Keys<typeof dataElementTypes>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @flow
import { convertClientToServer } from '../../converters';
import { convertClientToServer, convertAssigneeToServer } from '../../converters';
import { convertMainEvent } from './mainEventConverter';
import { dataElementTypes } from '../../metaData';
import { convertEventAttributeOptions } from '../convertEventAttributeOptions';
Expand All @@ -26,7 +26,7 @@ export function convertMainEventClientToServer(event: Object) {
convertedValue = convertClientToServer(value, dataElementTypes.DATE);
break;
case 'assignee':
convertedValue = value && ({ uid: value.id });
convertedValue = value && convertAssigneeToServer(value);
break;
default:
convertedValue = value;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// @flow
import type { UseQueryOptions } from 'react-query';
import { userStores, getUserStorageController } from '../../storageControllers';
import { useIndexedDBQuery } from '../reactQueryHelpers';
import type { CachedDataElement } from '../../storageControllers/';

export const useDataElementsFromIndexedDB = (queryKey: Array<string | number>, dataElementIds: ?Set<string>, queryOptions?: UseQueryOptions<>): {
dataElements: ?Array<CachedDataElement>,
isLoading: boolean,
isError: boolean,
} => {
const storageController = getUserStorageController();
const { enabled = !!dataElementIds } = queryOptions ?? {};

const { data, isLoading, isError } = useIndexedDBQuery(
['dataElements', ...queryKey],
() => storageController.getAll(
userStores.DATA_ELEMENTS, {
// $FlowIgnore - the enabled prop guarantees that dataElementIds will be defined
predicate: dataElement => dataElementIds.has(dataElement.id),
},
), {
enabled,
},
);

return {
dataElements: data,
isLoading,
isError,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// @flow
import type { UseQueryOptions } from 'react-query';
import { userStores, getUserStorageController } from '../../storageControllers';
import { useIndexedDBQuery } from '../reactQueryHelpers';
import type { CachedOptionSet } from '../../storageControllers/';

export const useOptionSetsFromIndexedDB = (queryKey: Array<string | number>, optionSetIds: ?Set<string>, queryOptions?: UseQueryOptions<>): {
optionSets: ?Array<CachedOptionSet>,
isLoading: boolean,
isError: boolean,
} => {
const storageController = getUserStorageController();
const { enabled = !!optionSetIds } = queryOptions ?? {};

const { data, isLoading, isError } = useIndexedDBQuery(
['optionSets', ...queryKey],
() => storageController.getAll(
userStores.OPTION_SETS, {
// $FlowIgnore - the enabled prop guarantees that optionSetIds will be defined
predicate: optionSet => optionSetIds.has(optionSet.id),
},
), {
enabled,
},
);

return {
optionSets: data,
isLoading,
isError,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { userStores, getUserStorageController } from '../../storageControllers';
import { useIndexedDBQuery } from '../reactQueryHelpers';


export const useProgramFromIndexedDB = (programId: ?string, QueryOptions?: UseQueryOptions<>) => {
export const useProgramFromIndexedDB = (programId: ?string, queryOptions?: UseQueryOptions<>) => {
const storageController = getUserStorageController();
const { enabled = true } = QueryOptions ?? {};
const { enabled = true } = queryOptions ?? {};

const { data, isLoading, isError } = useIndexedDBQuery(
// $FlowFixMe - only gets called when programId is defined because of enabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useDataEngine, type ResourceQuery } from '@dhis2/app-runtime';
import type { QueryFunction, UseQueryOptions } from 'react-query';
import { IndexedDBError } from '../../../../capture-core-utils/storage/IndexedDBError/IndexedDBError';
import type { Result } from './useMetadataQuery.types';
import { ReactQueryAppNamespace } from '../reactQueryHelpers.const';
import { ReactQueryAppNamespace, IndexedDBNamespace } from '../reactQueryHelpers.const';

const throwErrorForIndexedDB = (error) => {
if (error instanceof IndexedDBError) {
Expand Down Expand Up @@ -43,7 +43,7 @@ export const useIndexedDBQuery = <TResultData>(
queryFn: QueryFunction<TResultData>,
queryOptions?: UseQueryOptions<TResultData>,
): Result<TResultData> =>
useAsyncMetadata(queryKey, queryFn, {
useAsyncMetadata([IndexedDBNamespace, ...queryKey], queryFn, {
cacheTime: 0,
...queryOptions,
onError: (error) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

// @flow

export const ReactQueryAppNamespace = 'capture';
export const IndexedDBNamespace = 'indexedDB';

0 comments on commit 44fb2be

Please sign in to comment.