Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [DHIS-11419] display assigned users on events in enrollment overview page #3453

Merged
merged 11 commits into from
Dec 18, 2023
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';
Loading