From 73c63c5a07eabd940dcc5ce9739c8c2ab649469f Mon Sep 17 00:00:00 2001 From: farmerpaul Date: Tue, 27 Aug 2024 14:10:26 -0300 Subject: [PATCH 1/9] feat: add get my assignments API infra --- src/entities/assignment/api/index.ts | 1 + .../assignment/api/useMyAssignmentsQuery.ts | 15 ++++++++++ src/entities/assignment/index.ts | 1 + src/shared/api/services/assignment.service.ts | 12 ++++++++ src/shared/api/services/index.ts | 1 + src/shared/api/types/assignment.ts | 28 +++++++++++++++++++ src/shared/api/types/index.ts | 1 + src/shared/api/types/subject.ts | 2 ++ src/shared/utils/hooks/useFeatureFlags.ts | 1 + src/shared/utils/types/featureFlags.ts | 2 ++ .../lib/AppletDetailsContext.ts | 3 +- src/widgets/ActivityGroups/ui/index.tsx | 19 +++++++++++-- 12 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/entities/assignment/api/index.ts create mode 100644 src/entities/assignment/api/useMyAssignmentsQuery.ts create mode 100644 src/entities/assignment/index.ts create mode 100644 src/shared/api/services/assignment.service.ts create mode 100644 src/shared/api/types/assignment.ts diff --git a/src/entities/assignment/api/index.ts b/src/entities/assignment/api/index.ts new file mode 100644 index 000000000..54d136bb6 --- /dev/null +++ b/src/entities/assignment/api/index.ts @@ -0,0 +1 @@ +export * from './useMyAssignmentsQuery'; diff --git a/src/entities/assignment/api/useMyAssignmentsQuery.ts b/src/entities/assignment/api/useMyAssignmentsQuery.ts new file mode 100644 index 000000000..e0116cf77 --- /dev/null +++ b/src/entities/assignment/api/useMyAssignmentsQuery.ts @@ -0,0 +1,15 @@ +import { assignmentService, QueryOptions, ReturnAwaited, useBaseQuery } from '~/shared/api'; + +type FetchFn = typeof assignmentService.getMyAssignments; +type Options = QueryOptions; + +export const useMyAssignmentsQuery = >( + appletId?: string, + options?: Options, +) => { + return useBaseQuery( + ['myAssignments', { appletId }], + () => assignmentService.getMyAssignments({ appletId: String(appletId) }), + options, + ); +}; diff --git a/src/entities/assignment/index.ts b/src/entities/assignment/index.ts new file mode 100644 index 000000000..b1c13e734 --- /dev/null +++ b/src/entities/assignment/index.ts @@ -0,0 +1 @@ +export * from './api'; diff --git a/src/shared/api/services/assignment.service.ts b/src/shared/api/services/assignment.service.ts new file mode 100644 index 000000000..fbde0c834 --- /dev/null +++ b/src/shared/api/services/assignment.service.ts @@ -0,0 +1,12 @@ +import { axiosService } from '~/shared/api'; +import { GetMyAssignmentsPayload, MyAssignmentsSuccessResponse } from '~/shared/api/types'; + +function subjectService() { + return { + getMyAssignments({ appletId }: GetMyAssignmentsPayload) { + return axiosService.get(`/users/me/assignments/${appletId}`); + }, + }; +} + +export default subjectService(); diff --git a/src/shared/api/services/index.ts b/src/shared/api/services/index.ts index da150889f..72ecf878d 100644 --- a/src/shared/api/services/index.ts +++ b/src/shared/api/services/index.ts @@ -8,3 +8,4 @@ export { default as eventsService } from './events.service'; export { default as subjectService } from './subject.service'; export { default as answerService } from './answer.service'; export { default as workspaceService } from './workspace.service'; +export { default as assignmentService } from './assignment.service'; diff --git a/src/shared/api/types/assignment.ts b/src/shared/api/types/assignment.ts new file mode 100644 index 000000000..707697fec --- /dev/null +++ b/src/shared/api/types/assignment.ts @@ -0,0 +1,28 @@ +import { SubjectDTO } from './subject'; + +import { BaseSuccessResponse } from '~/shared/api/types/base'; +export interface GetMyAssignmentsPayload { + appletId: string; +} + +type AssignmentWithActivity = { + activityId: string; + activityFlowId: null; +}; + +type AssignmentWithFlow = { + activityId: null; + activityFlowId: string; +}; + +export type HydratedAssignmentDTO = (AssignmentWithActivity | AssignmentWithFlow) & { + respondentSubject: SubjectDTO; + targetSubject: SubjectDTO; +}; + +export type MyAssignmentsDTO = { + appletId: string; + assignments: HydratedAssignmentDTO[]; +}; + +export type MyAssignmentsSuccessResponse = BaseSuccessResponse; diff --git a/src/shared/api/types/index.ts b/src/shared/api/types/index.ts index 369885b06..a5617f0bf 100644 --- a/src/shared/api/types/index.ts +++ b/src/shared/api/types/index.ts @@ -6,3 +6,4 @@ export * from './activity'; export * from './events'; export * from './item'; export * from './conditionalLogic'; +export * from './assignment'; diff --git a/src/shared/api/types/subject.ts b/src/shared/api/types/subject.ts index 3cf65bd5c..7fca2a366 100644 --- a/src/shared/api/types/subject.ts +++ b/src/shared/api/types/subject.ts @@ -16,6 +16,8 @@ export type SubjectDTO = { lastSeen: string | null; id: string; userId: string | null; + firstName: string; + lastName: string; }; export type GetSubjectSuccessResponse = BaseSuccessResponse; diff --git a/src/shared/utils/hooks/useFeatureFlags.ts b/src/shared/utils/hooks/useFeatureFlags.ts index 5835306f0..a13063e5b 100644 --- a/src/shared/utils/hooks/useFeatureFlags.ts +++ b/src/shared/utils/hooks/useFeatureFlags.ts @@ -15,6 +15,7 @@ export const useFeatureFlags = () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment keys.forEach((key) => (features[key] = flags[FeatureFlagsKeys[key]])); + features.enableActivityAssign = false; return features; }; diff --git a/src/shared/utils/types/featureFlags.ts b/src/shared/utils/types/featureFlags.ts index 83818e1ea..571c22306 100644 --- a/src/shared/utils/types/featureFlags.ts +++ b/src/shared/utils/types/featureFlags.ts @@ -4,6 +4,8 @@ import { LDFlagValue } from 'launchdarkly-react-client-sdk'; // e.g. enable-participant-multi-informant in LaunchDarky becomes enableParticipantMultiInformant export const FeatureFlagsKeys = { enableParticipantMultiInformant: 'enableParticipantMultiInformant', + // TODO: https://mindlogger.atlassian.net/browse/M2-6518 Assign Activity flag cleanup + enableActivityAssign: 'enableActivityAssign', }; export type FeatureFlags = Partial>; diff --git a/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts b/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts index facb162a8..ce3bd2f2f 100644 --- a/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts +++ b/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts @@ -1,10 +1,11 @@ import { createContext } from 'react'; -import { AppletBaseDTO, AppletEventsResponse } from '~/shared/api'; +import { AppletBaseDTO, AppletEventsResponse, MyAssignmentsDTO } from '~/shared/api'; type AppletDetailsContextProps = { applet: AppletBaseDTO; events: AppletEventsResponse; + assignments: MyAssignmentsDTO['assignments']; }; type Public = { diff --git a/src/widgets/ActivityGroups/ui/index.tsx b/src/widgets/ActivityGroups/ui/index.tsx index 953fca8cd..eaf9cc0af 100644 --- a/src/widgets/ActivityGroups/ui/index.tsx +++ b/src/widgets/ActivityGroups/ui/index.tsx @@ -6,12 +6,14 @@ import { ActivityGroupList } from './ActivityGroupList'; import { AppletDetailsContext } from '../lib'; import { appletModel, useAppletBaseInfoByIdQuery } from '~/entities/applet'; +import { useMyAssignmentsQuery } from '~/entities/assignment'; import { useEventsbyAppletIdQuery } from '~/entities/event'; import { TakeNowSuccessModalProps } from '~/features/TakeNow/lib/types'; import { TakeNowSuccessModal } from '~/features/TakeNow/ui/TakeNowSuccessModal'; import Box from '~/shared/ui/Box'; import Loader from '~/shared/ui/Loader'; import { useCustomTranslation, useOnceEffect } from '~/shared/utils'; +import { useFeatureFlags } from '~/shared/utils/hooks/useFeatureFlags'; type PublicAppletDetails = { isPublic: true; @@ -35,6 +37,8 @@ export const ActivityGroups = (props: Props) => { useState({ isOpen: false, }); + const { featureFlags } = useFeatureFlags(); + const isAssignmentsEnabled = !!featureFlags.enableActivityAssign && !props.isPublic; const { isError: isAppletError, @@ -50,6 +54,15 @@ export const ActivityGroups = (props: Props) => { select: (data) => data.data.result, }); + const { + isError: isAssignmentsError, + isLoading: isAssignmentsLoading, + data: assignments = [], + } = useMyAssignmentsQuery(isAssignmentsEnabled ? props.appletId : undefined, { + select: (data) => data.data.result.assignments, + enabled: isAssignmentsEnabled, + }); + const { isInMultiInformantFlow, getMultiInformantState, @@ -77,11 +90,11 @@ export const ActivityGroups = (props: Props) => { } }); - if (isAppletLoading || isEventsLoading) { + if (isAppletLoading || isEventsLoading || (isAssignmentsEnabled && isAssignmentsLoading)) { return ; } - if (isEventsError || isAppletError) { + if (isEventsError || isAppletError || (isAssignmentsEnabled && isAssignmentsError)) { return ( {t('additional.invalid_public_url')} @@ -90,7 +103,7 @@ export const ActivityGroups = (props: Props) => { } return ( - + setTakeNowSuccessModalState({ isOpen: false })} From 47b863f1ed86f17b5120e4754cb5d07a4b2fde4b Mon Sep 17 00:00:00 2001 From: farmerpaul Date: Wed, 28 Aug 2024 11:03:19 -0300 Subject: [PATCH 2/9] refactor: activity groups processor In prep for adjusting `ActivityGroupsBuildManager` to accommodate assignments, refactor logic slightly to eliminate multiple calls to `filter`. Also rename identifiers to be make logic easier to follow and stay consistent with use of `EventEntity` type elsewhere in app. Also remove debug statement in `useFeatureFlags`. --- src/shared/utils/hooks/useFeatureFlags.ts | 1 - .../services/ActivityGroupsBuildManager.ts | 37 +++++++++---------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/shared/utils/hooks/useFeatureFlags.ts b/src/shared/utils/hooks/useFeatureFlags.ts index a13063e5b..5835306f0 100644 --- a/src/shared/utils/hooks/useFeatureFlags.ts +++ b/src/shared/utils/hooks/useFeatureFlags.ts @@ -15,7 +15,6 @@ export const useFeatureFlags = () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment keys.forEach((key) => (features[key] = flags[FeatureFlagsKeys[key]])); - features.enableActivityAssign = false; return features; }; diff --git a/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts b/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts index 041f04026..3d3bad986 100644 --- a/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts +++ b/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts @@ -48,11 +48,9 @@ const createActivityGroupsBuildManager = () => { const process = (params: ProcessParams): BuildResult => { const activities: Activity[] = mapActivitiesFromDto(params.activities); - const activityFlows: ActivityFlow[] = mapActivityFlowsFromDto(params.flows); const eventsResponse = params.events; - const events: ScheduleEvent[] = EventModel.mapEventsFromDto(eventsResponse.events); const idToEntity = buildIdToEntityMap(activities, activityFlows); @@ -62,30 +60,29 @@ const createActivityGroupsBuildManager = () => { progress: params.entityProgress, }); - let entityEvents = events - .map((event) => ({ - entity: idToEntity[event.entityId], - event, - })) - // @todo - remove after fix on BE - .filter((entityEvent) => !!entityEvent.entity); - + let eventEntities: EventEntity[] = []; const calculator = EventModel.ScheduledDateCalculator; - for (const eventActivity of entityEvents) { - const date = calculator.calculate(eventActivity.event); - eventActivity.event.scheduledAt = date; - } + for (const event of events) { + const entity = idToEntity[event.entityId]; + if (!entity || entity.isHidden) continue; + + event.scheduledAt = calculator.calculate(event); + if (!event.scheduledAt) continue; - entityEvents = entityEvents.filter((x) => x.event.scheduledAt); + const eventEntity: EventEntity = { + entity, + event, + }; - entityEvents = entityEvents.filter((x) => !x.entity.isHidden); + eventEntities.push(eventEntity); + } - entityEvents = sort(entityEvents); + eventEntities = sort(eventEntities); - const groupAvailable = builder.buildAvailable(entityEvents); - const groupInProgress = builder.buildInProgress(entityEvents); - const groupScheduled = builder.buildScheduled(entityEvents); + const groupAvailable = builder.buildAvailable(eventEntities); + const groupInProgress = builder.buildInProgress(eventEntities); + const groupScheduled = builder.buildScheduled(eventEntities); return { groups: [groupInProgress, groupAvailable, groupScheduled], From eaa4a5ce7628b54e357d4373646e5140698011d3 Mon Sep 17 00:00:00 2001 From: farmerpaul Date: Wed, 28 Aug 2024 16:06:27 -0300 Subject: [PATCH 3/9] fix: use consistent naming of `EventEntity` types --- .../lib/GroupBuilder/ActivityGroupsBuilder.ts | 24 ++++++------- src/abstract/lib/GroupBuilder/GroupUtility.ts | 36 +++++++++---------- .../lib/GroupBuilder/ListItemsFactory.ts | 28 +++++++-------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/abstract/lib/GroupBuilder/ActivityGroupsBuilder.ts b/src/abstract/lib/GroupBuilder/ActivityGroupsBuilder.ts index 414c80889..0b088718b 100644 --- a/src/abstract/lib/GroupBuilder/ActivityGroupsBuilder.ts +++ b/src/abstract/lib/GroupBuilder/ActivityGroupsBuilder.ts @@ -32,13 +32,13 @@ export class ActivityGroupsBuilder implements IActivityGroupsBuilder { this.utility = new GroupUtility(inputParams); } - public buildInProgress(eventsActivities: Array): ActivityListGroup { - const filtered = eventsActivities.filter((x) => this.utility.isInProgress(x)); + public buildInProgress(eventEntities: Array): ActivityListGroup { + const filtered = eventEntities.filter((x) => this.utility.isInProgress(x)); const activityItems: Array = []; - for (const eventActivity of filtered) { - const item = this.itemsFactory.createProgressItem(eventActivity); + for (const eventEntity of filtered) { + const item = this.itemsFactory.createProgressItem(eventEntity); activityItems.push(item); } @@ -52,13 +52,13 @@ export class ActivityGroupsBuilder implements IActivityGroupsBuilder { return result; } - public buildAvailable(eventsEntities: Array): ActivityListGroup { - const filtered = this.availableEvaluator.evaluate(eventsEntities); + public buildAvailable(eventEntities: Array): ActivityListGroup { + const filtered = this.availableEvaluator.evaluate(eventEntities); const activityItems: Array = []; - for (const eventActivity of filtered) { - const item = this.itemsFactory.createAvailableItem(eventActivity); + for (const eventEntity of filtered) { + const item = this.itemsFactory.createAvailableItem(eventEntity); activityItems.push(item); } @@ -72,13 +72,13 @@ export class ActivityGroupsBuilder implements IActivityGroupsBuilder { return result; } - public buildScheduled(eventsEntities: Array): ActivityListGroup { - const filtered = this.scheduledEvaluator.evaluate(eventsEntities); + public buildScheduled(eventEntities: Array): ActivityListGroup { + const filtered = this.scheduledEvaluator.evaluate(eventEntities); const activityItems: Array = []; - for (const eventActivity of filtered) { - const item = this.itemsFactory.createScheduledItem(eventActivity); + for (const eventEntity of filtered) { + const item = this.itemsFactory.createScheduledItem(eventEntity); activityItems.push(item); } diff --git a/src/abstract/lib/GroupBuilder/GroupUtility.ts b/src/abstract/lib/GroupBuilder/GroupUtility.ts index 8455cb37d..57e5bd4d0 100644 --- a/src/abstract/lib/GroupBuilder/GroupUtility.ts +++ b/src/abstract/lib/GroupBuilder/GroupUtility.ts @@ -45,11 +45,11 @@ export class GroupUtility { } private getAllowedTimeInterval( - eventActivity: EventEntity, + eventEntity: EventEntity, scheduledWhen: 'today' | 'yesterday', isAccessBeforeStartTime = false, ): DatesFromTo { - const { event } = eventActivity; + const { event } = eventEntity; if (event.availability.timeFrom === null) { throw new Error('[getAllowedTimeInterval] timeFrom is null'); @@ -117,19 +117,19 @@ export class GroupUtility { return isEqual(this.getYesterday(), startOfDay(date)); } - public getProgressRecord(eventActivity: EventEntity): GroupProgress | null { - const record = this.progress[getProgressId(eventActivity.entity.id, eventActivity.event.id)]; + public getProgressRecord(eventEntity: EventEntity): GroupProgress | null { + const record = this.progress[getProgressId(eventEntity.entity.id, eventEntity.event.id)]; return record ?? null; } - public getCompletedAt(eventActivity: EventEntity): Date | null { - const progressRecord = this.getProgressRecord(eventActivity); + public getCompletedAt(eventEntity: EventEntity): Date | null { + const progressRecord = this.getProgressRecord(eventEntity); return progressRecord?.endAt ? new Date(progressRecord.endAt) : null; } - public isInProgress(eventActivity: EventEntity): boolean { - const record = this.getProgressRecord(eventActivity); + public isInProgress(eventEntity: EventEntity): boolean { + const record = this.getProgressRecord(eventEntity); if (!record) { return false; } @@ -210,17 +210,17 @@ export class GroupUtility { } public isCompletedInAllowedTimeInterval( - eventActivity: EventEntity, + eventEntity: EventEntity, scheduledWhen: 'today' | 'yesterday', isAccessBeforeStartTime = false, ): boolean { const { from: allowedFrom, to: allowedTo } = this.getAllowedTimeInterval( - eventActivity, + eventEntity, scheduledWhen, isAccessBeforeStartTime, ); - const completedAt = this.getCompletedAt(eventActivity); + const completedAt = this.getCompletedAt(eventEntity); if (!completedAt) { return false; @@ -276,19 +276,19 @@ export class GroupUtility { return false; } - public isCompletedToday(eventActivity: EventEntity): boolean { - const date = this.getCompletedAt(eventActivity); + public isCompletedToday(eventEntity: EventEntity): boolean { + const date = this.getCompletedAt(eventEntity); return !!date && this.isToday(date); } public isInAllowedTimeInterval( - eventActivity: EventEntity, + eventEntity: EventEntity, scheduledWhen: 'today' | 'yesterday', isAccessBeforeStartTime = false, ): boolean { const { from: allowedFrom, to: allowedTo } = this.getAllowedTimeInterval( - eventActivity, + eventEntity, scheduledWhen, isAccessBeforeStartTime, ); @@ -302,15 +302,15 @@ export class GroupUtility { } } - public getTimeToComplete(eventActivity: EventEntity): HourMinute | null { - const { event } = eventActivity; + public getTimeToComplete(eventEntity: EventEntity): HourMinute | null { + const { event } = eventEntity; const timer = event.timers.timer; if (timer === null) { throw new Error('[getTimeToComplete] Timer is null'); } - const startedTime = this.getStartedAt(eventActivity); + const startedTime = this.getStartedAt(eventEntity); const activityDuration: number = getMsFromHours(timer.hours) + getMsFromMinutes(timer.minutes); diff --git a/src/abstract/lib/GroupBuilder/ListItemsFactory.ts b/src/abstract/lib/GroupBuilder/ListItemsFactory.ts index 7338e241b..f1c0c2fe7 100644 --- a/src/abstract/lib/GroupBuilder/ListItemsFactory.ts +++ b/src/abstract/lib/GroupBuilder/ListItemsFactory.ts @@ -52,9 +52,9 @@ export class ListItemsFactory { item.image = activity.image; } - private createListItem(eventActivity: EventEntity) { - const { entity, event } = eventActivity; - const { pipelineType } = eventActivity.entity; + private createListItem(eventEntity: EventEntity) { + const { entity, event } = eventEntity; + const { pipelineType } = eventEntity.entity; const isFlow = pipelineType === ActivityPipelineType.Flow; const item: ActivityListItem = { @@ -76,17 +76,17 @@ export class ListItemsFactory { }; if (isFlow) { - this.populateActivityFlowFields(item, eventActivity); + this.populateActivityFlowFields(item, eventEntity); } return item; } - public createAvailableItem(eventActivity: EventEntity): ActivityListItem { - const item = this.createListItem(eventActivity); + public createAvailableItem(eventEntity: EventEntity): ActivityListItem { + const item = this.createListItem(eventEntity); item.status = ActivityStatus.Available; - const { event } = eventActivity; + const { event } = eventEntity; if (event.availability.availabilityType === AvailabilityLabelType.ScheduledAccess) { const isSpread = this.utility.isSpreadToNextDay(event); @@ -116,12 +116,12 @@ export class ListItemsFactory { return item; } - public createScheduledItem(eventActivity: EventEntity): ActivityListItem { - const item = this.createListItem(eventActivity); + public createScheduledItem(eventEntity: EventEntity): ActivityListItem { + const item = this.createListItem(eventEntity); item.status = ActivityStatus.Scheduled; - const { event } = eventActivity; + const { event } = eventEntity; const from = this.utility.getNow(); @@ -153,12 +153,12 @@ export class ListItemsFactory { return item; } - public createProgressItem(eventActivity: EventEntity): ActivityListItem { - const item = this.createListItem(eventActivity); + public createProgressItem(eventEntity: EventEntity): ActivityListItem { + const item = this.createListItem(eventEntity); item.status = ActivityStatus.InProgress; - const { event } = eventActivity; + const { event } = eventEntity; if (event.availability.availabilityType === AvailabilityLabelType.ScheduledAccess) { const isSpread = this.utility.isSpreadToNextDay(event); @@ -181,7 +181,7 @@ export class ListItemsFactory { item.isTimerSet = !!event.timers?.timer; if (item.isTimerSet) { - const timeLeft = this.utility.getTimeToComplete(eventActivity); + const timeLeft = this.utility.getTimeToComplete(eventEntity); item.timeLeftToComplete = timeLeft; if (timeLeft === null) { From 4a02258bbe567e11dcdbfa38f17898b35d8d4afd Mon Sep 17 00:00:00 2001 From: farmerpaul Date: Wed, 28 Aug 2024 16:24:34 -0300 Subject: [PATCH 4/9] feat: incorporate assignments into activities list Update logic to take into account both auto-assigned and manually assigned activities and flows when generating list of activities/flows available on the applet's home screen. Falls back to existing approach if `enableActivityAssign` feature flag is disabled. Refactor activity/flow sorting into a single array `sort` call using a custom comparer, instead of the less efficient and harder to read approach involving multiple `filter` calls, a custom `sort`, followed by recominbation. This change doesn't account for tracking distinct in-progress states for each assignment of the same activity, or assignment-specific one-time completion status; that will be done in an upcoming commit. --- .../lib/GroupBuilder/ListItemsFactory.ts | 3 +- .../lib/GroupBuilder/activityGroups.types.ts | 4 + src/abstract/lib/GroupBuilder/types.ts | 2 + src/shared/api/types/applet.ts | 2 + .../lib/AppletDetailsContext.ts | 3 +- .../ActivityGroups/model/hooks/index.ts | 2 +- ...eActivityGroup.ts => useActivityGroups.ts} | 12 +-- src/widgets/ActivityGroups/model/mappers.ts | 12 ++- .../services/ActivityGroupsBuildManager.ts | 93 +++++++++++++++---- .../ActivityGroups/ui/ActivityGroupList.tsx | 7 +- src/widgets/ActivityGroups/ui/index.tsx | 2 +- 11 files changed, 105 insertions(+), 37 deletions(-) rename src/widgets/ActivityGroups/model/hooks/{useActivityGroup.ts => useActivityGroups.ts} (65%) diff --git a/src/abstract/lib/GroupBuilder/ListItemsFactory.ts b/src/abstract/lib/GroupBuilder/ListItemsFactory.ts index f1c0c2fe7..0fa5142fa 100644 --- a/src/abstract/lib/GroupBuilder/ListItemsFactory.ts +++ b/src/abstract/lib/GroupBuilder/ListItemsFactory.ts @@ -53,7 +53,7 @@ export class ListItemsFactory { } private createListItem(eventEntity: EventEntity) { - const { entity, event } = eventEntity; + const { entity, event, targetSubject } = eventEntity; const { pipelineType } = eventEntity.entity; const isFlow = pipelineType === ActivityPipelineType.Flow; @@ -61,6 +61,7 @@ export class ListItemsFactory { activityId: isFlow ? '' : entity.id, flowId: isFlow ? entity.id : null, eventId: event.id, + targetSubject, name: isFlow ? '' : entity.name, description: isFlow ? '' : entity.description, type: isFlow ? ActivityType.NotDefined : (entity as Activity).type, diff --git a/src/abstract/lib/GroupBuilder/activityGroups.types.ts b/src/abstract/lib/GroupBuilder/activityGroups.types.ts index 61b6d295c..55a4410c1 100644 --- a/src/abstract/lib/GroupBuilder/activityGroups.types.ts +++ b/src/abstract/lib/GroupBuilder/activityGroups.types.ts @@ -1,6 +1,7 @@ import { ActivityPipelineType } from '~/abstract/lib'; import { ActivityType } from '~/abstract/lib/GroupBuilder'; import { ScheduleEvent } from '~/entities/event'; +import { SubjectDTO } from '~/shared/api/types/subject'; export type EntityBase = { id: string; @@ -9,6 +10,7 @@ export type EntityBase = { image: string | null; isHidden: boolean; order: number; + autoAssign: boolean; }; export type Activity = EntityBase & { @@ -27,6 +29,8 @@ export type Entity = Activity | ActivityFlow; export type EventEntity = { entity: Entity; event: ScheduleEvent; + /** Target subject of assignment if not self-report, else undefined */ + targetSubject?: SubjectDTO; }; export type EntityType = 'regular' | 'flow'; diff --git a/src/abstract/lib/GroupBuilder/types.ts b/src/abstract/lib/GroupBuilder/types.ts index 45ecd0c4b..ba873e5ba 100644 --- a/src/abstract/lib/GroupBuilder/types.ts +++ b/src/abstract/lib/GroupBuilder/types.ts @@ -1,10 +1,12 @@ import { AvailabilityLabelType } from '~/entities/event'; +import { SubjectDTO } from '~/shared/api/types/subject'; import { HourMinute } from '~/shared/utils'; export type ActivityListItem = { activityId: string; flowId: string | null; eventId: string; + targetSubject?: SubjectDTO; name: string; description: string; diff --git a/src/shared/api/types/applet.ts b/src/shared/api/types/applet.ts index 9bb516053..90dda1d66 100644 --- a/src/shared/api/types/applet.ts +++ b/src/shared/api/types/applet.ts @@ -77,6 +77,7 @@ export type ActivityBaseDTO = { order: number; containsResponseTypes: Array; itemCount: number; + autoAssign: boolean; }; type Integration = 'loris'; @@ -105,6 +106,7 @@ export type ActivityFlowDTO = { order: number; isHidden: boolean; activityIds: Array; + autoAssign: boolean; }; export type EventAvailabilityDto = { diff --git a/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts b/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts index ce3bd2f2f..45ecd79f7 100644 --- a/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts +++ b/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts @@ -5,7 +5,8 @@ import { AppletBaseDTO, AppletEventsResponse, MyAssignmentsDTO } from '~/shared/ type AppletDetailsContextProps = { applet: AppletBaseDTO; events: AppletEventsResponse; - assignments: MyAssignmentsDTO['assignments']; + /** Activity assignments array - undefined if activity assign is disabled */ + assignments?: MyAssignmentsDTO['assignments']; }; type Public = { diff --git a/src/widgets/ActivityGroups/model/hooks/index.ts b/src/widgets/ActivityGroups/model/hooks/index.ts index 6954604c3..c77189a09 100644 --- a/src/widgets/ActivityGroups/model/hooks/index.ts +++ b/src/widgets/ActivityGroups/model/hooks/index.ts @@ -1,4 +1,4 @@ -export * from './useActivityGroup'; +export * from './useActivityGroups'; export * from './useEntitiesSync'; export * from './useEntityCardDetails'; export * from './useIntegrationsSync'; diff --git a/src/widgets/ActivityGroups/model/hooks/useActivityGroup.ts b/src/widgets/ActivityGroups/model/hooks/useActivityGroups.ts similarity index 65% rename from src/widgets/ActivityGroups/model/hooks/useActivityGroup.ts rename to src/widgets/ActivityGroups/model/hooks/useActivityGroups.ts index f011a4c9a..a800c9f63 100644 --- a/src/widgets/ActivityGroups/model/hooks/useActivityGroup.ts +++ b/src/widgets/ActivityGroups/model/hooks/useActivityGroups.ts @@ -2,29 +2,29 @@ import { ActivityGroupsBuildManager } from '../services/ActivityGroupsBuildManag import { ActivityListGroup } from '~/abstract/lib/GroupBuilder'; import { appletModel } from '~/entities/applet'; -import { AppletBaseDTO, AppletEventsResponse } from '~/shared/api'; +import { AppletBaseDTO, AppletEventsResponse, HydratedAssignmentDTO } from '~/shared/api'; import { useAppSelector } from '~/shared/utils'; type Props = { applet: AppletBaseDTO; events: AppletEventsResponse; + assignments?: HydratedAssignmentDTO[]; }; type Return = { groups: ActivityListGroup[]; }; -export const useActivityGroups = ({ applet, events }: Props): Return => { +export const useActivityGroups = ({ applet, events, assignments }: Props): Return => { const groupsInProgress = useAppSelector(appletModel.selectors.groupProgressSelector); - const groupsResult = ActivityGroupsBuildManager.process({ + const { groups } = ActivityGroupsBuildManager.process({ activities: applet.activities, flows: applet.activityFlows, events, + assignments, entityProgress: groupsInProgress, }); - return { - groups: groupsResult.groups, - }; + return { groups }; }; diff --git a/src/widgets/ActivityGroups/model/mappers.ts b/src/widgets/ActivityGroups/model/mappers.ts index 091239d9b..4591ecb21 100644 --- a/src/widgets/ActivityGroups/model/mappers.ts +++ b/src/widgets/ActivityGroups/model/mappers.ts @@ -4,11 +4,12 @@ import { ActivityBaseDTO, ActivityFlowDTO } from '~/shared/api'; export const mapActivitiesFromDto = (dtos: ActivityBaseDTO[]): Activity[] => { return dtos.map((dto) => ({ - id: dto.id, + autoAssign: dto.autoAssign, description: dto.description, + id: dto.id, image: dto.image, - name: dto.name, isHidden: dto.isHidden, + name: dto.name, order: dto.order, pipelineType: ActivityPipelineType.Regular, type: ActivityType.NotDefined, @@ -18,13 +19,14 @@ export const mapActivitiesFromDto = (dtos: ActivityBaseDTO[]): Activity[] => { export const mapActivityFlowsFromDto = (dtos: ActivityFlowDTO[]): ActivityFlow[] => { return dtos.map((dto) => ({ activityIds: dto.activityIds, + autoAssign: dto.autoAssign, description: dto.description, hideBadge: dto.hideBadge, - order: dto.order, id: dto.id, - name: dto.name, + image: null, isHidden: dto.isHidden, + name: dto.name, + order: dto.order, pipelineType: ActivityPipelineType.Flow, - image: null, })); }; diff --git a/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts b/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts index 3d3bad986..0c9b49af4 100644 --- a/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts +++ b/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts @@ -10,7 +10,12 @@ import { createActivityGroupsBuilder, } from '~/abstract/lib/GroupBuilder'; import { EventModel, ScheduleEvent } from '~/entities/event'; -import { ActivityBaseDTO, ActivityFlowDTO, AppletEventsResponse } from '~/shared/api'; +import { + ActivityBaseDTO, + ActivityFlowDTO, + AppletEventsResponse, + HydratedAssignmentDTO, +} from '~/shared/api'; type BuildResult = { groups: ActivityListGroup[]; @@ -19,6 +24,7 @@ type BuildResult = { type ProcessParams = { activities: ActivityBaseDTO[]; flows: ActivityFlowDTO[]; + assignments: HydratedAssignmentDTO[] | undefined; events: AppletEventsResponse; entityProgress: GroupProgressState; }; @@ -34,16 +40,50 @@ const createActivityGroupsBuildManager = () => { }, {}); }; - const sort = (eventEntities: EventEntity[]) => { - let flows = eventEntities.filter((x) => x.entity.pipelineType === ActivityPipelineType.Flow); - let activities = eventEntities.filter( - (x) => x.entity.pipelineType === ActivityPipelineType.Regular, - ); - - flows = flows.sort((a, b) => a.entity.order - b.entity.order); - activities = activities.sort((a, b) => a.entity.order - b.entity.order); + const buildIdToAssignmentsMap = ( + assignments: HydratedAssignmentDTO[], + ): Record => { + return assignments.reduce>((acc, current) => { + const key = current.activityFlowId ?? current.activityId; + if (acc[key]) { + acc[key].push(current); + } else { + acc[key] = [current]; + } + return acc; + }, {}); + }; - return [...flows, ...activities]; + const sort = (eventEntities: EventEntity[]) => { + eventEntities.sort((a: EventEntity, b: EventEntity) => { + const aIsFlow = a.entity.pipelineType === ActivityPipelineType.Flow; + const bIsFlow = b.entity.pipelineType === ActivityPipelineType.Flow; + + // Order flows first + if (aIsFlow && !bIsFlow) { + return -1; + } else if (!aIsFlow && bIsFlow) { + return 1; + } + + // Then order by entity order + const orderDiff = a.entity.order - b.entity.order; + if (orderDiff !== 0) return orderDiff; + + // Then order self-reports first + if (!a.targetSubject) { + return -1; + } else if (!b.targetSubject) { + return 1; + } + + // Then order by target subject first name + const firstNameDiff = a.targetSubject.firstName.localeCompare(b.targetSubject.firstName); + if (firstNameDiff !== 0) return firstNameDiff; + + // Then order by target subject last name + return a.targetSubject.lastName.localeCompare(b.targetSubject.lastName); + }); }; const process = (params: ProcessParams): BuildResult => { @@ -54,13 +94,14 @@ const createActivityGroupsBuildManager = () => { const events: ScheduleEvent[] = EventModel.mapEventsFromDto(eventsResponse.events); const idToEntity = buildIdToEntityMap(activities, activityFlows); + const idToAssignments = params.assignments ? buildIdToAssignmentsMap(params.assignments) : {}; const builder = createActivityGroupsBuilder({ allAppletActivities: activities, progress: params.entityProgress, }); - let eventEntities: EventEntity[] = []; + const eventEntities: EventEntity[] = []; const calculator = EventModel.ScheduledDateCalculator; for (const event of events) { @@ -70,15 +111,31 @@ const createActivityGroupsBuildManager = () => { event.scheduledAt = calculator.calculate(event); if (!event.scheduledAt) continue; - const eventEntity: EventEntity = { - entity, - event, - }; - - eventEntities.push(eventEntity); + if (params.assignments) { + const assignments = idToAssignments[entity.id] ?? []; + + // Add auto-assignment if enabled for this activity/flow + if (entity.autoAssign) { + eventEntities.push({ entity, event }); + } + + // Add any additional assignments (without duplicating possible auto-assignment) + for (const { respondentSubject, targetSubject } of assignments) { + const isSelfAssign = respondentSubject.id === targetSubject.id; + if (entity.autoAssign && isSelfAssign) continue; + + eventEntities.push(isSelfAssign ? { entity, event } : { entity, event, targetSubject }); + } + } else { + // Assignments disabled + eventEntities.push({ + entity, + event, + }); + } } - eventEntities = sort(eventEntities); + sort(eventEntities); const groupAvailable = builder.buildAvailable(eventEntities); const groupInProgress = builder.buildInProgress(eventEntities); diff --git a/src/widgets/ActivityGroups/ui/ActivityGroupList.tsx b/src/widgets/ActivityGroups/ui/ActivityGroupList.tsx index 2418c26f7..b8f125e9e 100644 --- a/src/widgets/ActivityGroups/ui/ActivityGroupList.tsx +++ b/src/widgets/ActivityGroups/ui/ActivityGroupList.tsx @@ -9,8 +9,7 @@ import { useActivityGroups, useEntitiesSync, useIntegrationsSync } from '../mode import AppletDefaultIcon from '~/assets/AppletDefaultIcon.svg'; import { useCompletedEntitiesQuery } from '~/entities/activity'; -import { BootstrapModal } from '~/shared/ui'; -import { AvatarBase } from '~/shared/ui'; +import { AvatarBase, BootstrapModal } from '~/shared/ui'; import Box from '~/shared/ui/Box'; import Loader from '~/shared/ui/Loader'; import Text from '~/shared/ui/Text'; @@ -19,7 +18,7 @@ import { formatToDtoDate, useCustomTranslation } from '~/shared/utils'; export const ActivityGroupList = () => { const { t } = useCustomTranslation(); - const { applet, events, isPublic } = useContext(AppletDetailsContext); + const { applet, events, assignments, isPublic } = useContext(AppletDetailsContext); useIntegrationsSync({ appletDetails: applet }); @@ -38,7 +37,7 @@ export const ActivityGroupList = () => { const isAppletAboutExist = Boolean(applet?.about); const isAppletImageExist = Boolean(applet?.image); - const { groups } = useActivityGroups({ applet, events }); + const { groups } = useActivityGroups({ applet, events, assignments }); const onCardAboutClick = () => { if (!isAppletAboutExist) return; diff --git a/src/widgets/ActivityGroups/ui/index.tsx b/src/widgets/ActivityGroups/ui/index.tsx index eaf9cc0af..7e6faa558 100644 --- a/src/widgets/ActivityGroups/ui/index.tsx +++ b/src/widgets/ActivityGroups/ui/index.tsx @@ -57,7 +57,7 @@ export const ActivityGroups = (props: Props) => { const { isError: isAssignmentsError, isLoading: isAssignmentsLoading, - data: assignments = [], + data: assignments, } = useMyAssignmentsQuery(isAssignmentsEnabled ? props.appletId : undefined, { select: (data) => data.data.result.assignments, enabled: isAssignmentsEnabled, From 2b4cf90accd38a6789df3fdc25f028982caac16b Mon Sep 17 00:00:00 2001 From: farmerpaul Date: Wed, 28 Aug 2024 18:41:47 -0300 Subject: [PATCH 5/9] feat: add target subject label to activity cards Make shared `TargetSubjectLabel` component which will also be used in the activity screens. Also fix missing translations and missing/incorrect use of i18next plural forms in several `ActivityLabel` subcomponents. --- src/assets/subject-icon.svg | 5 +++ src/i18n/en/translation.json | 16 ++++--- src/i18n/fr/translation.json | 16 ++++--- src/shared/utils/helpers/getSubjectName.ts | 7 +++ src/shared/utils/helpers/index.ts | 1 + .../ui/ActivityCard/ActivityCardBase.tsx | 2 +- .../ActivityLabel/ActivityAvailableLabel.tsx | 11 ++--- .../ActivityFlowAvailableLabel.tsx | 4 +- .../ActivityFlowInProgressLabel.tsx | 2 +- .../ActivityLabel/ActivityInProgressLabel.tsx | 2 +- .../ActivityLabel/ActivityLabelTypography.tsx | 9 +++- .../ActivityGroups/ui/ActivityCard/index.tsx | 25 +++++++---- .../ActivityGroups/ui/ActivityGroup.tsx | 7 ++- src/widgets/Survey/ui/ActivityMetaData.tsx | 6 +-- src/widgets/TargetSubjectLabel/index.tsx | 43 +++++++++++++++++++ 15 files changed, 115 insertions(+), 41 deletions(-) create mode 100644 src/assets/subject-icon.svg create mode 100644 src/shared/utils/helpers/getSubjectName.ts create mode 100644 src/widgets/TargetSubjectLabel/index.tsx diff --git a/src/assets/subject-icon.svg b/src/assets/subject-icon.svg new file mode 100644 index 000000000..f7438492a --- /dev/null +++ b/src/assets/subject-icon.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json index dd058719c..85f5bd6a4 100644 --- a/src/i18n/en/translation.json +++ b/src/i18n/en/translation.json @@ -19,14 +19,17 @@ "pleaseProvideAdditionalText": "Please provide additional text", "pleaseListenToAudio": "Please listen to the audio until the end.", "onlyNumbersAllowed": "Only numbers are allowed", - "question_count_plural": "{{length}} questions", - "question_count_singular": "{{length}} question", + "questionCount_one": "{{count}} question", + "questionCount_other": "{{count}} questions", "timedActivityTitle": "is a Timed Activity.", "youWillHaveToCompleteIt": "You will have {{hours}} hours {{minutes}} minutes to complete it.", "yourWorkWillBeSubmitted": "Your work will be auto-submitted when time runs out.", - "countOfCompletedQuestions": "{{countOfCompletedQuestions}} of {{length}} questions completed", - "activityFlowLength": "{{length}} activities", - "countOfCompletedActivities": "{{countOfCompletedActivities}} of {{length}} activities completed", + "countOfCompletedQuestions_one": "{{countOfCompletedQuestions}} of {{count}} question completed", + "countOfCompletedQuestions_other": "{{countOfCompletedQuestions}} of {{count}} questions completed", + "activityFlowLength_one": "{{count}} activity", + "activityFlowLength_other": "{{count}} activities", + "countOfCompletedActivities_one": "{{countOfCompletedActivities}} of {{count}} activity completed", + "countOfCompletedActivities_other": "{{countOfCompletedActivities}} of {{count}} activities completed", "pleaseCompleteOnThe": "Please complete on the", "mindloggerMobileApp": "MindLogger mobile app", "mustBeCompletedUsingMobileApp": "Please use the MindLogger mobile app", @@ -305,6 +308,7 @@ "successModalPrimaryAction": "Return to Admin App", "successModalSecondaryAction": "Dismiss" }, - "charactersCount": "{{numCharacters}}/{{maxCharacters}} characters" + "charactersCount": "{{numCharacters}}/{{maxCharacters}} characters", + "targetSubjectLabel": "About {{name}}" } } diff --git a/src/i18n/fr/translation.json b/src/i18n/fr/translation.json index 049f3f164..2aafb3414 100644 --- a/src/i18n/fr/translation.json +++ b/src/i18n/fr/translation.json @@ -23,11 +23,14 @@ "timedActivityTitle": "est une activité chronométrée.", "youWillHaveToCompleteIt": "Vous aurez {{hours}} heures {{minutes}} minutes pour le terminer.", "yourWorkWillBeSubmitted": "Votre travail sera automatiquement soumis lorsque le temps imparti sera écoulé.", - "question_count_plural": "{{length}} questions", - "question_count_singular": "{{length}} question", - "countOfCompletedQuestions": "{{countOfCompletedQuestions}} of {{length}} questions completed", - "activityFlowLength": "{{length}} activities", - "countOfCompletedActivities": "{{countOfCompletedActivities}} of {{length}} activities completed", + "questionCount_one": "{{count}} question", + "questionCount_other": "{{count}} questions", + "countOfCompletedQuestions_one": "{{countOfCompletedQuestions}} sur {{count}} question complétée", + "countOfCompletedQuestions_other": "{{countOfCompletedQuestions}} sur {{count}} questions complétées", + "activityFlowLength_one": "{{count}} activité", + "activityFlowLength_other": "{{count}} activités", + "countOfCompletedActivities_one": "{{countOfCompletedActivities}} sur {{count}} activité complétée", + "countOfCompletedActivities_other": "{{countOfCompletedActivities}} sur {{count}} activités complétées", "pleaseCompleteOnThe": "Please complete on the", "mindloggerMobileApp": "MindLogger mobile app", @@ -324,6 +327,7 @@ "successModalPrimaryAction": "Revenir à l'application d'administration", "successModalSecondaryAction": "Rejeter" }, - "charactersCount": "{{numCharacters}}/{{maxCharacters}} caractères" + "charactersCount": "{{numCharacters}}/{{maxCharacters}} caractères", + "targetSubjectLabel": "À propos de {{name}}" } } diff --git a/src/shared/utils/helpers/getSubjectName.ts b/src/shared/utils/helpers/getSubjectName.ts new file mode 100644 index 000000000..6ee1b0e9f --- /dev/null +++ b/src/shared/utils/helpers/getSubjectName.ts @@ -0,0 +1,7 @@ +import { SubjectDTO } from '~/shared/api/types/subject'; + +export const getSubjectName = ({ firstName, lastName }: SubjectDTO) => { + const lastInitial = lastName[0] ? ` ${lastName[0]}.` : ''; + + return `${firstName}${lastInitial}`; +}; diff --git a/src/shared/utils/helpers/index.ts b/src/shared/utils/helpers/index.ts index b56839b20..5027f16d8 100644 --- a/src/shared/utils/helpers/index.ts +++ b/src/shared/utils/helpers/index.ts @@ -6,3 +6,4 @@ export * from './splitList'; export * from './getInitials'; export * from './delay'; export * from './cutString'; +export * from './getSubjectName'; diff --git a/src/widgets/ActivityGroups/ui/ActivityCard/ActivityCardBase.tsx b/src/widgets/ActivityGroups/ui/ActivityCard/ActivityCardBase.tsx index a75a1f9c4..a7d709dfc 100644 --- a/src/widgets/ActivityGroups/ui/ActivityCard/ActivityCardBase.tsx +++ b/src/widgets/ActivityGroups/ui/ActivityCard/ActivityCardBase.tsx @@ -16,7 +16,7 @@ export const ActivityCardBase = (props: Props) => { sx={{ backgroundColor: Theme.colors.light.surface, padding: '24px', - marginBottom: '36px', + marginBottom: '16px', border: `1px solid ${Theme.colors.light.surfaceVariant}`, borderRadius: '16px', minWidth: '343px', diff --git a/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityAvailableLabel.tsx b/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityAvailableLabel.tsx index 751f13ccc..2f4be073f 100644 --- a/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityAvailableLabel.tsx +++ b/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityAvailableLabel.tsx @@ -11,12 +11,6 @@ type Props = { export const ActivityAvailableLabel = (props: Props) => { const { t } = useCustomTranslation(); - const isActivitiesMoreThanOne = props.activityLength > 1; - - const activityLabel = isActivitiesMoreThanOne - ? t('question_count_plural', { length: props.activityLength }) - : t('question_count_singular', { length: props.activityLength }); - return ( { backgroundColor: Theme.colors.light.primary95, }} > - + ); }; diff --git a/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityFlowAvailableLabel.tsx b/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityFlowAvailableLabel.tsx index 8255feb0d..8ab078646 100644 --- a/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityFlowAvailableLabel.tsx +++ b/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityFlowAvailableLabel.tsx @@ -28,9 +28,7 @@ export const ActivityFlowAvailableLabel = ({ activityFlowLength }: Props) => { > diff --git a/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityFlowInProgressLabel.tsx b/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityFlowInProgressLabel.tsx index eaf6d42c2..54c5b68c5 100644 --- a/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityFlowInProgressLabel.tsx +++ b/src/widgets/ActivityGroups/ui/ActivityCard/ActivityLabel/ActivityFlowInProgressLabel.tsx @@ -30,7 +30,7 @@ export const ActivityFlowInProgressLabel = (props: Props) => { { > { return ( - + {text} ); diff --git a/src/widgets/ActivityGroups/ui/ActivityCard/index.tsx b/src/widgets/ActivityGroups/ui/ActivityCard/index.tsx index 8407b7809..64c5bba85 100644 --- a/src/widgets/ActivityGroups/ui/ActivityCard/index.tsx +++ b/src/widgets/ActivityGroups/ui/ActivityCard/index.tsx @@ -26,6 +26,7 @@ import { MixpanelPayload, useOnceLayoutEffect, } from '~/shared/utils'; +import { TargetSubjectLabel } from '~/widgets/TargetSubjectLabel'; type Props = { activityListItem: ActivityListItem; @@ -177,15 +178,21 @@ export const ActivityCard = ({ activityListItem }: Props) => { /> )} - + + + + {!!activityListItem.targetSubject && ( + + )} + {description && } diff --git a/src/widgets/ActivityGroups/ui/ActivityGroup.tsx b/src/widgets/ActivityGroups/ui/ActivityGroup.tsx index 1d9d10ce7..3fc8f45ab 100644 --- a/src/widgets/ActivityGroups/ui/ActivityGroup.tsx +++ b/src/widgets/ActivityGroups/ui/ActivityGroup.tsx @@ -33,7 +33,12 @@ export const ActivityGroup = ({ group }: Props) => { {group.activities.map((activity) => { - return ; + return ( + + ); })} diff --git a/src/widgets/Survey/ui/ActivityMetaData.tsx b/src/widgets/Survey/ui/ActivityMetaData.tsx index 0d92bad18..1a300c2c3 100644 --- a/src/widgets/Survey/ui/ActivityMetaData.tsx +++ b/src/widgets/Survey/ui/ActivityMetaData.tsx @@ -11,11 +11,7 @@ type Props = { export const ActivityMetaData = ({ activityLength, isFlow, activityOrderInFlow }: Props) => { const { t } = useCustomTranslation(); - const isActivitiesMoreThanOne = activityLength > 1; - - const activityLengthLabel = isActivitiesMoreThanOne - ? t('question_count_plural', { length: activityLength }) - : t('question_count_singular', { length: activityLength }); + const activityLengthLabel = t('questionCount', { count: activityLength }); if (!isFlow && !activityOrderInFlow) { return <>{activityLengthLabel}; diff --git a/src/widgets/TargetSubjectLabel/index.tsx b/src/widgets/TargetSubjectLabel/index.tsx new file mode 100644 index 000000000..c013c245d --- /dev/null +++ b/src/widgets/TargetSubjectLabel/index.tsx @@ -0,0 +1,43 @@ +import { Avatar } from '@mui/material'; + +import SubjectIcon from '~/assets/subject-icon.svg'; +import { SubjectDTO } from '~/shared/api/types/subject'; +import { Theme } from '~/shared/constants'; +import { Box, Text } from '~/shared/ui'; +import { getSubjectName, useCustomTranslation } from '~/shared/utils'; + +type Props = { + subject: SubjectDTO; +}; + +export const TargetSubjectLabel = ({ subject }: Props) => { + const { t } = useCustomTranslation(); + + const name = getSubjectName(subject); + + return ( + + + + {t('targetSubjectLabel', { name })} + + + ); +}; From 60f907138f10f802a25054f7c8aa037f5ad71995 Mon Sep 17 00:00:00 2001 From: farmerpaul Date: Fri, 30 Aug 2024 10:42:19 -0300 Subject: [PATCH 6/9] feat: per-subject activity progress/submission Made widespread change to incorporate `targetSubjectId` into activity progres tracking, when assignments feature is enabled. Anytime `eventId` is used, now `targetSubjectid` also needs to be passed to identify which activity assignment is the active assessment. This value is also passed to the `answers` POST endpoint to ensure the submission is also properly associated with the `targetSubjectId`. For self-reports, and when assignments feature is disabled, `targetSubjectId` is set to `null`, effectively inheriting existing functionality. For `PublicSurvey` and `PublicAutoCompletion`, `targetSubjectId` stays `null` since public assessments do not support activity assignments. --- src/abstract/lib/GroupBuilder/GroupUtility.ts | 5 +- .../lib/GroupBuilder/activityGroups.types.ts | 4 +- src/abstract/lib/GroupBuilder/types.ts | 2 +- src/abstract/lib/getProgressId.ts | 18 ++- src/abstract/lib/types/entityProgress.ts | 8 +- .../applet/model/hooks/useActivityProgress.ts | 10 +- .../applet/model/hooks/useEntityComplete.ts | 36 ++++- .../applet/model/hooks/useEntityStart.ts | 32 +++-- .../model/hooks/useGroupProgressRecord.ts | 9 +- .../hooks/useGroupProgressStateManager.ts | 5 +- .../applet/model/hooks/useItemTimerState.ts | 19 ++- .../model/hooks/useSaveActivityItemAnswer.ts | 9 +- .../applet/model/hooks/useUserEvents.ts | 14 +- src/entities/applet/model/selectors.ts | 9 +- src/entities/applet/model/slice.ts | 131 +++++++++--------- src/entities/applet/model/types.ts | 16 +++ src/entities/subject/api/useSubjectQuery.ts | 4 +- .../model/hooks/useAutoCompletionRecord.ts | 9 +- .../hooks/useAutoCompletionStateManager.ts | 3 +- src/features/AutoCompletion/model/slice.ts | 13 +- src/features/PassSurvey/hooks/useAnswers.ts | 8 +- src/features/PassSurvey/hooks/useItemTimer.ts | 3 + .../PassSurvey/hooks/useStartSurvey.ts | 23 ++- .../PassSurvey/hooks/useSummaryData.ts | 4 +- .../PassSurvey/hooks/useSurveyDataQuery.ts | 30 +++- src/features/PassSurvey/lib/SurveyContext.ts | 2 + src/features/PassSurvey/lib/mappers.ts | 5 +- src/features/PassSurvey/ui/EntityTimer.tsx | 2 +- src/pages/AutoCompletion/index.tsx | 2 + src/pages/PublicAutoCompletion/index.tsx | 1 + src/pages/PublicSurvey/index.tsx | 1 + src/pages/Survey/index.tsx | 9 +- src/shared/api/types/activity.ts | 1 + src/shared/constants/routes.ts | 28 +++- src/shared/utils/hooks/index.ts | 1 + .../lib/AppletDetailsContext.ts | 4 +- .../model/hooks/useActivityGroups.ts | 2 +- .../model/hooks/useEntitiesSync.ts | 4 + .../services/ActivityGroupsBuildManager.ts | 11 +- .../ActivityGroups/ui/ActivityCard/index.tsx | 8 +- src/widgets/ActivityGroups/ui/index.tsx | 4 +- .../AutoCompletion/lib/useAutoCompletion.ts | 4 + .../ui/AutoCompletionScreen.tsx | 4 + src/widgets/AutoCompletion/ui/index.tsx | 23 ++- .../Survey/model/hooks/useEntityTimer.ts | 3 +- src/widgets/Survey/ui/PassingScreen.tsx | 47 ++++++- src/widgets/Survey/ui/ScreenManager.tsx | 4 +- src/widgets/Survey/ui/SummaryScreen/index.tsx | 4 +- src/widgets/Survey/ui/WelcomeScreen.tsx | 14 +- src/widgets/Survey/ui/index.tsx | 22 ++- 50 files changed, 456 insertions(+), 178 deletions(-) diff --git a/src/abstract/lib/GroupBuilder/GroupUtility.ts b/src/abstract/lib/GroupBuilder/GroupUtility.ts index 57e5bd4d0..6f651cf57 100644 --- a/src/abstract/lib/GroupBuilder/GroupUtility.ts +++ b/src/abstract/lib/GroupBuilder/GroupUtility.ts @@ -118,7 +118,10 @@ export class GroupUtility { } public getProgressRecord(eventEntity: EventEntity): GroupProgress | null { - const record = this.progress[getProgressId(eventEntity.entity.id, eventEntity.event.id)]; + const record = + this.progress[ + getProgressId(eventEntity.entity.id, eventEntity.event.id, eventEntity.targetSubject?.id) + ]; return record ?? null; } diff --git a/src/abstract/lib/GroupBuilder/activityGroups.types.ts b/src/abstract/lib/GroupBuilder/activityGroups.types.ts index 55a4410c1..943fc4133 100644 --- a/src/abstract/lib/GroupBuilder/activityGroups.types.ts +++ b/src/abstract/lib/GroupBuilder/activityGroups.types.ts @@ -29,8 +29,8 @@ export type Entity = Activity | ActivityFlow; export type EventEntity = { entity: Entity; event: ScheduleEvent; - /** Target subject of assignment if not self-report, else undefined */ - targetSubject?: SubjectDTO; + /** Target subject of assignment if not self-report, else null */ + targetSubject: SubjectDTO | null; }; export type EntityType = 'regular' | 'flow'; diff --git a/src/abstract/lib/GroupBuilder/types.ts b/src/abstract/lib/GroupBuilder/types.ts index ba873e5ba..8551a473c 100644 --- a/src/abstract/lib/GroupBuilder/types.ts +++ b/src/abstract/lib/GroupBuilder/types.ts @@ -6,7 +6,7 @@ export type ActivityListItem = { activityId: string; flowId: string | null; eventId: string; - targetSubject?: SubjectDTO; + targetSubject: SubjectDTO | null; name: string; description: string; diff --git a/src/abstract/lib/getProgressId.ts b/src/abstract/lib/getProgressId.ts index 7fe5c6ea9..f05acd2c2 100644 --- a/src/abstract/lib/getProgressId.ts +++ b/src/abstract/lib/getProgressId.ts @@ -1,11 +1,17 @@ -export const getProgressId = (entityId: string, eventId: string): string => { - return `${entityId}/${eventId}`; +import { GroupProgressId } from './types'; + +export const getProgressId = ( + entityId: string, + eventId: string, + targetSubjectId?: string | null, +): GroupProgressId => { + return targetSubjectId ? `${entityId}/${eventId}/${targetSubjectId}` : `${entityId}/${eventId}`; }; export const getDataFromProgressId = ( - progressId: string, -): { entityId: string; eventId: string } => { - const [entityId, eventId] = progressId.split('/'); + progressId: GroupProgressId, +): { entityId: string; eventId: string; targetSubjectId: string | null } => { + const [entityId, eventId, targetSubjectId = null] = progressId.split('/'); - return { entityId, eventId }; + return { entityId, eventId, targetSubjectId }; }; diff --git a/src/abstract/lib/types/entityProgress.ts b/src/abstract/lib/types/entityProgress.ts index 58634ba22..3a0554d6e 100644 --- a/src/abstract/lib/types/entityProgress.ts +++ b/src/abstract/lib/types/entityProgress.ts @@ -55,6 +55,12 @@ type EventProgressTimestampState = { export type GroupProgress = ActivityOrFlowProgress & EventProgressTimestampState; -type GroupProgressId = string; // Group progress id is a combination of entityId and eventId (EntityId = ActivityId or FlowId) +/** + * Combination of: + * - entityId (= activityId/flowId), + * - eventId + * - targetSubjectId (optional; only if not self-report) + */ +export type GroupProgressId = `${string}/${string}` | `${string}/${string}/${string}`; export type GroupProgressState = Record; diff --git a/src/entities/applet/model/hooks/useActivityProgress.ts b/src/entities/applet/model/hooks/useActivityProgress.ts index 1ae971a67..3735f0883 100644 --- a/src/entities/applet/model/hooks/useActivityProgress.ts +++ b/src/entities/applet/model/hooks/useActivityProgress.ts @@ -12,11 +12,13 @@ import { useAppDispatch, useAppSelector } from '~/shared/utils'; type SaveProgressProps = { activity: ActivityDTO; eventId: string; + targetSubjectId: string | null; }; type DefaultProps = { activityId: string; eventId: string; + targetSubjectId: string | null; }; export const useActivityProgress = () => { @@ -26,7 +28,10 @@ export const useActivityProgress = () => { const getActivityProgress = useCallback( (props: DefaultProps): ActivityProgress | null => { - const progress = activitiesProgressState[getProgressId(props.activityId, props.eventId)]; + const progress = + activitiesProgressState[ + getProgressId(props.activityId, props.eventId, props.targetSubjectId) + ]; return progress ?? null; }, @@ -61,6 +66,7 @@ export const useActivityProgress = () => { actions.saveActivityProgress({ activityId: props.activity.id, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, progress: { items: preparedActivityItemProgressRecords, step: initialStep, @@ -81,6 +87,7 @@ export const useActivityProgress = () => { actions.changeSummaryScreenVisibility({ activityId: props.activityId, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, isSummaryScreenOpen: true, }), ); @@ -94,6 +101,7 @@ export const useActivityProgress = () => { actions.changeSummaryScreenVisibility({ activityId: props.activityId, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, isSummaryScreenOpen: false, }), ); diff --git a/src/entities/applet/model/hooks/useEntityComplete.ts b/src/entities/applet/model/hooks/useEntityComplete.ts index cd33dd01d..175ac6227 100644 --- a/src/entities/applet/model/hooks/useEntityComplete.ts +++ b/src/entities/applet/model/hooks/useEntityComplete.ts @@ -13,6 +13,7 @@ type CompletionType = 'regular' | 'autoCompletion'; type Props = { activityId: string; eventId: string; + targetSubjectId: string | null; flowId: string | null; publicAppletKey: string | null; @@ -40,6 +41,7 @@ export const useEntityComplete = (props: Props) => { entityCompleted({ entityId: props.flowId ? props.flowId : props.activityId, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, }); const isAutoCompletion = completionType === 'autoCompletion'; @@ -73,6 +75,7 @@ export const useEntityComplete = (props: Props) => { props.eventId, props.flowId, props.publicAppletKey, + props.targetSubjectId, ], ); @@ -97,13 +100,21 @@ export const useEntityComplete = (props: Props) => { appletId: props.appletId, activityId, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, entityType: 'flow', flowId: props.flowId, }), { replace: true }, ); }, - [navigator, props.appletId, props.eventId, props.flowId, props.publicAppletKey], + [ + navigator, + props.appletId, + props.eventId, + props.flowId, + props.publicAppletKey, + props.targetSubjectId, + ], ); const completeFlow = useCallback( @@ -113,6 +124,7 @@ export const useEntityComplete = (props: Props) => { const groupProgress = getGroupProgress({ entityId: props.flowId ? props.flowId : props.activityId, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, }); if (!groupProgress) { @@ -137,10 +149,15 @@ export const useEntityComplete = (props: Props) => { activityId: nextActivityId ? nextActivityId : props.flow.activityIds[0], flowId: props.flow.id, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, pipelineActivityOrder: nextActivityId ? currentPipelineActivityOrder + 1 : 0, }); - removeActivityProgress({ activityId: props.activityId, eventId: props.eventId }); + removeActivityProgress({ + activityId: props.activityId, + eventId: props.eventId, + targetSubjectId: props.targetSubjectId, + }); if (nextActivityId && !isAutoCompletion) { return redirectToNextActivity(nextActivityId); @@ -158,6 +175,7 @@ export const useEntityComplete = (props: Props) => { props.eventId, props.flow, props.flowId, + props.targetSubjectId, redirectToNextActivity, removeActivityProgress, ], @@ -165,11 +183,21 @@ export const useEntityComplete = (props: Props) => { const completeActivity = useCallback( (input?: CompleteOptions) => { - removeActivityProgress({ activityId: props.activityId, eventId: props.eventId }); + removeActivityProgress({ + activityId: props.activityId, + eventId: props.eventId, + targetSubjectId: props.targetSubjectId, + }); return completeEntityAndRedirect(input?.type || 'regular'); }, - [completeEntityAndRedirect, props.activityId, props.eventId, removeActivityProgress], + [ + completeEntityAndRedirect, + props.activityId, + props.eventId, + props.targetSubjectId, + removeActivityProgress, + ], ); return { diff --git a/src/entities/applet/model/hooks/useEntityStart.ts b/src/entities/applet/model/hooks/useEntityStart.ts index d4f6902a0..ea6f9d8cc 100644 --- a/src/entities/applet/model/hooks/useEntityStart.ts +++ b/src/entities/applet/model/hooks/useEntityStart.ts @@ -9,16 +9,24 @@ export const useEntityStart = () => { const dispatch = useAppDispatch(); const groupProgress = useAppSelector(groupProgressSelector); - const getProgress = (entityId: string, eventId: string): GroupProgress => - groupProgress[getProgressId(entityId, eventId)]; + const getProgress = ( + entityId: string, + eventId: string, + targetSubjectId: string | null, + ): GroupProgress => groupProgress[getProgressId(entityId, eventId, targetSubjectId)]; const isInProgress = (payload: GroupProgress): boolean => payload && !payload.endAt; - function activityStarted(activityId: string, eventId: string): void { + function activityStarted( + activityId: string, + eventId: string, + targetSubjectId: string | null, + ): void { dispatch( actions.activityStarted({ activityId, eventId, + targetSubjectId, }), ); } @@ -27,6 +35,7 @@ export const useEntityStart = () => { flowId: string, activityId: string, eventId: string, + targetSubjectId: string | null, pipelineActivityOrder: number, ): void { dispatch( @@ -34,25 +43,30 @@ export const useEntityStart = () => { flowId, activityId, eventId, + targetSubjectId, pipelineActivityOrder, }), ); } - function startActivity(activityId: string, eventId: string): void { - const isActivityInProgress = isInProgress(getProgress(activityId, eventId)); + function startActivity( + activityId: string, + eventId: string, + targetSubjectId: string | null, + ): void { + const isActivityInProgress = isInProgress(getProgress(activityId, eventId, targetSubjectId)); if (isActivityInProgress) { return; } - return activityStarted(activityId, eventId); + return activityStarted(activityId, eventId, targetSubjectId); } - function startFlow(eventId: string, flow: ActivityFlowDTO): void { + function startFlow(eventId: string, flow: ActivityFlowDTO, targetSubjectId: string | null): void { const flowId = flow.id; - const isFlowInProgress = isInProgress(getProgress(flowId, eventId)); + const isFlowInProgress = isInProgress(getProgress(flowId, eventId, targetSubjectId)); if (isFlowInProgress) { return; @@ -68,7 +82,7 @@ export const useEntityStart = () => { const firstActivityId: string = flowActivities[0]; - return flowStarted(flowId, firstActivityId, eventId, 0); + return flowStarted(flowId, firstActivityId, eventId, targetSubjectId, 0); } return { startActivity, startFlow }; diff --git a/src/entities/applet/model/hooks/useGroupProgressRecord.ts b/src/entities/applet/model/hooks/useGroupProgressRecord.ts index 7a76bbddf..aabdf99c6 100644 --- a/src/entities/applet/model/hooks/useGroupProgressRecord.ts +++ b/src/entities/applet/model/hooks/useGroupProgressRecord.ts @@ -6,11 +6,16 @@ import { useAppSelector } from '~/shared/utils'; type Props = { entityId: string; eventId: string; + targetSubjectId: string | null; }; -export const useGroupProgressRecord = ({ entityId, eventId }: Props): GroupProgress | null => { +export const useGroupProgressRecord = ({ + entityId, + eventId, + targetSubjectId, +}: Props): GroupProgress | null => { const record = useAppSelector((state) => - selectGroupProgress(state, getProgressId(entityId, eventId)), + selectGroupProgress(state, getProgressId(entityId, eventId, targetSubjectId)), ); return record ?? null; diff --git a/src/entities/applet/model/hooks/useGroupProgressStateManager.ts b/src/entities/applet/model/hooks/useGroupProgressStateManager.ts index 86c186f3f..49f5e30fa 100644 --- a/src/entities/applet/model/hooks/useGroupProgressStateManager.ts +++ b/src/entities/applet/model/hooks/useGroupProgressStateManager.ts @@ -33,7 +33,10 @@ export const useGroupProgressStateManager = (): Return => { return null; } - return groupProgresses[getProgressId(params.entityId, params.eventId)] ?? null; + return ( + groupProgresses[getProgressId(params.entityId, params.eventId, params.targetSubjectId)] ?? + null + ); }, [groupProgresses], ); diff --git a/src/entities/applet/model/hooks/useItemTimerState.ts b/src/entities/applet/model/hooks/useItemTimerState.ts index ed2677572..8e1c788dc 100644 --- a/src/entities/applet/model/hooks/useItemTimerState.ts +++ b/src/entities/applet/model/hooks/useItemTimerState.ts @@ -10,6 +10,7 @@ import { useAppDispatch, useAppSelector } from '~/shared/utils'; type Props = { activityId: string; eventId: string; + targetSubjectId: string | null; itemId: string; }; @@ -24,11 +25,16 @@ type InitializeTimerProps = { duration: number; }; -export const useItemTimerState = ({ activityId, eventId, itemId }: Props): Return => { +export const useItemTimerState = ({ + activityId, + eventId, + targetSubjectId, + itemId, +}: Props): Return => { const dispatch = useAppDispatch(); const timerSettingsState = useAppSelector((state) => - selectActivityProgress(state, getProgressId(activityId, eventId)), + selectActivityProgress(state, getProgressId(activityId, eventId, targetSubjectId)), ); const timerSettings = timerSettingsState?.itemTimer[itemId] ?? null; @@ -39,6 +45,7 @@ export const useItemTimerState = ({ activityId, eventId, itemId }: Props): Retur actions.setItemTimerStatus({ activityId, eventId, + targetSubjectId, itemId, timerStatus: { duration, @@ -47,7 +54,7 @@ export const useItemTimerState = ({ activityId, eventId, itemId }: Props): Retur }), ); }, - [activityId, dispatch, eventId, itemId], + [activityId, dispatch, eventId, itemId, targetSubjectId], ); const removeTimer = useCallback(() => { @@ -55,20 +62,22 @@ export const useItemTimerState = ({ activityId, eventId, itemId }: Props): Retur actions.removeItemTimerStatus({ activityId, eventId, + targetSubjectId, itemId, }), ); - }, [activityId, dispatch, eventId, itemId]); + }, [activityId, dispatch, eventId, itemId, targetSubjectId]); const timerTick = useCallback(() => { return dispatch( actions.itemTimerTick({ activityId, eventId, + targetSubjectId, itemId, }), ); - }, [activityId, dispatch, eventId, itemId]); + }, [activityId, dispatch, eventId, itemId, targetSubjectId]); return { timerSettings, diff --git a/src/entities/applet/model/hooks/useSaveActivityItemAnswer.ts b/src/entities/applet/model/hooks/useSaveActivityItemAnswer.ts index 2de0447b0..f29096e53 100644 --- a/src/entities/applet/model/hooks/useSaveActivityItemAnswer.ts +++ b/src/entities/applet/model/hooks/useSaveActivityItemAnswer.ts @@ -8,9 +8,10 @@ import { useAppDispatch } from '~/shared/utils'; type Props = { activityId: string; eventId: string; + targetSubjectId: string | null; }; -export const useSaveItemAnswer = ({ activityId, eventId }: Props) => { +export const useSaveItemAnswer = ({ activityId, eventId, targetSubjectId }: Props) => { const dispatch = useAppDispatch(); const saveItemAnswer = useCallback( @@ -19,12 +20,13 @@ export const useSaveItemAnswer = ({ activityId, eventId }: Props) => { actions.saveItemAnswer({ entityId: activityId, eventId, + targetSubjectId, itemId, answer, }), ); }, - [dispatch, activityId, eventId], + [dispatch, activityId, eventId, targetSubjectId], ); const saveItemAdditionalText = useCallback( @@ -33,12 +35,13 @@ export const useSaveItemAnswer = ({ activityId, eventId }: Props) => { actions.saveAdditionalText({ entityId: activityId, eventId, + targetSubjectId, itemId, additionalText, }), ); }, - [dispatch, activityId, eventId], + [dispatch, activityId, eventId, targetSubjectId], ); const removeItemAnswer = useCallback( diff --git a/src/entities/applet/model/hooks/useUserEvents.ts b/src/entities/applet/model/hooks/useUserEvents.ts index 4190e7347..5603c58bf 100644 --- a/src/entities/applet/model/hooks/useUserEvents.ts +++ b/src/entities/applet/model/hooks/useUserEvents.ts @@ -12,12 +12,13 @@ import { useAppDispatch, useAppSelector } from '~/shared/utils'; type Props = { activityId: string; eventId: string; + targetSubjectId: string | null; }; export const useUserEvents = (props: Props) => { const dispatch = useAppDispatch(); - const progressId = getProgressId(props.activityId, props.eventId); + const progressId = getProgressId(props.activityId, props.eventId, props.targetSubjectId); const activityProgress = useAppSelector((state) => selectActivityProgress(state, progressId)); @@ -35,6 +36,7 @@ export const useUserEvents = (props: Props) => { actions.saveUserEvent({ entityId: props.activityId, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, itemId: item.id, userEvent: newUserEvent, }), @@ -42,7 +44,7 @@ export const useUserEvents = (props: Props) => { return newUserEvent; }, - [dispatch, props.activityId, props.eventId], + [dispatch, props.activityId, props.eventId, props.targetSubjectId], ); const saveSetAnswerUserEvent = useCallback( @@ -65,6 +67,7 @@ export const useUserEvents = (props: Props) => { actions.updateUserEventByIndex({ entityId: props.activityId, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, userEventIndex: userEvents.length - 1, userEvent: { type: 'SET_ANSWER', @@ -83,6 +86,7 @@ export const useUserEvents = (props: Props) => { actions.saveUserEvent({ entityId: props.activityId, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, itemId: item.id, userEvent: { type: 'SET_ANSWER', @@ -93,7 +97,7 @@ export const useUserEvents = (props: Props) => { }), ); }, - [activityProgress, dispatch, props.activityId, props.eventId], + [activityProgress, dispatch, props.activityId, props.eventId, props.targetSubjectId], ); const saveSetAdditionalTextUserEvent = useCallback( @@ -128,6 +132,7 @@ export const useUserEvents = (props: Props) => { actions.updateUserEventByIndex({ entityId: props.activityId, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, userEventIndex: userEvents.length - 1, userEvent: { type: 'SET_ANSWER', @@ -148,6 +153,7 @@ export const useUserEvents = (props: Props) => { actions.saveUserEvent({ entityId: props.activityId, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, itemId: item.id, userEvent: { type: 'SET_ANSWER', @@ -158,7 +164,7 @@ export const useUserEvents = (props: Props) => { }), ); }, - [activityProgress, dispatch, props.activityId, props.eventId], + [activityProgress, dispatch, props.activityId, props.eventId, props.targetSubjectId], ); return { saveUserEventByType, saveSetAnswerUserEvent, saveSetAdditionalTextUserEvent }; diff --git a/src/entities/applet/model/selectors.ts b/src/entities/applet/model/selectors.ts index a533a55f2..6b6717ea2 100644 --- a/src/entities/applet/model/selectors.ts +++ b/src/entities/applet/model/selectors.ts @@ -1,8 +1,9 @@ import { createSelector } from '@reduxjs/toolkit'; +import { GroupProgressId } from '~/abstract/lib'; import { RootState } from '~/shared/utils'; -const selectEntityId = (_: unknown, entityId: string) => entityId; +const selectGroupProgressId = (_: unknown, groupProgressId: GroupProgressId) => groupProgressId; export const appletsSelector = (state: RootState) => state.applets; @@ -12,8 +13,8 @@ export const groupProgressSelector = createSelector( ); export const selectGroupProgress = createSelector( - [groupProgressSelector, selectEntityId], - (groupProgress, entityId) => groupProgress[entityId], + [groupProgressSelector, selectGroupProgressId], + (groupProgress, groupProgressId) => groupProgress[groupProgressId], ); export const activityProgressSelector = createSelector( @@ -22,7 +23,7 @@ export const activityProgressSelector = createSelector( ); export const selectActivityProgress = createSelector( - [activityProgressSelector, selectEntityId], + [activityProgressSelector, selectGroupProgressId], (progress, entityId) => progress[entityId], ); diff --git a/src/entities/applet/model/slice.ts b/src/entities/applet/model/slice.ts index 81fe1c67c..d9acf721b 100644 --- a/src/entities/applet/model/slice.ts +++ b/src/entities/applet/model/slice.ts @@ -60,60 +60,63 @@ const appletsSlice = createSlice({ return initialState; }, - removeActivityProgress: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.activityId, action.payload.eventId); + removeActivityProgress: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.activityId, payload.eventId, payload.targetSubjectId); delete state.progress[id]; }, - saveGroupProgress: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.activityId, action.payload.eventId); + saveGroupProgress: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.activityId, payload.eventId, payload.targetSubjectId); const groupProgress = state.groupProgress[id] ?? {}; const updatedProgress = { ...groupProgress, - ...action.payload.progressPayload, + ...payload.progressPayload, }; state.groupProgress[id] = updatedProgress; }, - removeGroupProgress: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.entityId, action.payload.eventId); + removeGroupProgress: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.entityId, payload.eventId, payload.targetSubjectId); delete state.groupProgress[id]; }, - saveSummaryDataInGroupContext: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.entityId, action.payload.eventId); + saveSummaryDataInGroupContext: ( + state, + { payload }: PayloadAction, + ) => { + const id = getProgressId(payload.entityId, payload.eventId, payload.targetSubjectId); const groupProgress = state.groupProgress[id] ?? {}; const groupContext = groupProgress.context ?? {}; - groupContext.summaryData[action.payload.activityId] = action.payload.summaryData; + groupContext.summaryData[payload.activityId] = payload.summaryData; }, - saveActivityProgress: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.activityId, action.payload.eventId); + saveActivityProgress: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.activityId, payload.eventId, payload.targetSubjectId); - state.progress[id] = action.payload.progress; + state.progress[id] = payload.progress; }, changeSummaryScreenVisibility: ( state, - action: PayloadAction, + { payload }: PayloadAction, ) => { - const id = getProgressId(action.payload.activityId, action.payload.eventId); + const id = getProgressId(payload.activityId, payload.eventId, payload.targetSubjectId); const activityProgress = state.progress[id]; - activityProgress.isSummaryScreenOpen = action.payload.isSummaryScreenOpen; + activityProgress.isSummaryScreenOpen = payload.isSummaryScreenOpen; }, - setItemTimerStatus: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.activityId, action.payload.eventId); + setItemTimerStatus: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.activityId, payload.eventId, payload.targetSubjectId); const progress = state.progress[id]; @@ -121,31 +124,31 @@ const appletsSlice = createSlice({ progress.itemTimer = {}; } - progress.itemTimer[action.payload.itemId] = action.payload.timerStatus; + progress.itemTimer[payload.itemId] = payload.timerStatus; }, - removeItemTimerStatus: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.activityId, action.payload.eventId); + removeItemTimerStatus: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.activityId, payload.eventId, payload.targetSubjectId); const progress = state.progress[id]; if (!progress) return state; - const timer = progress.itemTimer[action.payload.itemId]; + const timer = progress.itemTimer[payload.itemId]; if (timer) { - delete progress.itemTimer[action.payload.itemId]; + delete progress.itemTimer[payload.itemId]; } }, - itemTimerTick: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.activityId, action.payload.eventId); + itemTimerTick: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.activityId, payload.eventId, payload.targetSubjectId); const progress = state.progress[id]; if (!progress) return state; - const timer = progress.itemTimer[action.payload.itemId]; + const timer = progress.itemTimer[payload.itemId]; if (!timer) return state; @@ -154,21 +157,21 @@ const appletsSlice = createSlice({ } }, - saveItemAnswer: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.entityId, action.payload.eventId); + saveItemAnswer: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.entityId, payload.eventId, payload.targetSubjectId); const activityProgress = state.progress[id]; if (!activityProgress) { return state; } - const itemIndex = activityProgress.items.findIndex(({ id }) => id === action.payload.itemId); + const itemIndex = activityProgress.items.findIndex(({ id }) => id === payload.itemId); if (itemIndex === -1) { return state; } - activityProgress.items[itemIndex].answer = action.payload.answer; + activityProgress.items[itemIndex].answer = payload.answer; }, /** @@ -177,66 +180,66 @@ const appletsSlice = createSlice({ * @param action * @returns */ - saveAdditionalText: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.entityId, action.payload.eventId); + saveAdditionalText: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.entityId, payload.eventId, payload.targetSubjectId); const activityProgress = state.progress[id]; if (!activityProgress) { return state; } - const itemIndex = activityProgress.items.findIndex(({ id }) => id === action.payload.itemId); + const itemIndex = activityProgress.items.findIndex(({ id }) => id === payload.itemId); if (itemIndex === -1) { return state; } - activityProgress.items[itemIndex].additionalText = action.payload.additionalText; + activityProgress.items[itemIndex].additionalText = payload.additionalText; }, - saveUserEvent: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.entityId, action.payload.eventId); + saveUserEvent: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.entityId, payload.eventId, payload.targetSubjectId); const activityProgress = state.progress[id]; if (!activityProgress) { return state; } - activityProgress.userEvents.push(action.payload.userEvent); + activityProgress.userEvents.push(payload.userEvent); }, - updateUserEventByIndex: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.entityId, action.payload.eventId); + updateUserEventByIndex: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.entityId, payload.eventId, payload.targetSubjectId); const activityProgress = state.progress[id]; if (!activityProgress) { return state; } - if (!activityProgress.userEvents[action.payload.userEventIndex]) { + if (!activityProgress.userEvents[payload.userEventIndex]) { return state; } - activityProgress.userEvents[action.payload.userEventIndex] = action.payload.userEvent; + activityProgress.userEvents[payload.userEventIndex] = payload.userEvent; }, - incrementStep: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.activityId, action.payload.eventId); + incrementStep: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.activityId, payload.eventId, payload.targetSubjectId); const activityProgress = state.progress[id]; activityProgress.step += 1; }, - decrementStep: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.activityId, action.payload.eventId); + decrementStep: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.activityId, payload.eventId, payload.targetSubjectId); const activityProgress = state.progress[id]; activityProgress.step -= 1; }, - activityStarted: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.activityId, action.payload.eventId); + activityStarted: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.activityId, payload.eventId, payload.targetSubjectId); const activityEvent: GroupProgress = { type: ActivityPipelineType.Regular, @@ -250,17 +253,17 @@ const appletsSlice = createSlice({ state.groupProgress[id] = activityEvent; }, - flowStarted: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.flowId, action.payload.eventId); + flowStarted: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.flowId, payload.eventId, payload.targetSubjectId); const flowEvent: GroupProgress = { type: ActivityPipelineType.Flow, - currentActivityId: action.payload.activityId, + currentActivityId: payload.activityId, startAt: new Date().getTime(), currentActivityStartAt: new Date().getTime(), endAt: null, executionGroupKey: uuidV4(), - pipelineActivityOrder: action.payload.pipelineActivityOrder, + pipelineActivityOrder: payload.pipelineActivityOrder, context: { summaryData: {}, }, @@ -269,18 +272,18 @@ const appletsSlice = createSlice({ state.groupProgress[id] = flowEvent; }, - flowUpdated: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.flowId, action.payload.eventId); + flowUpdated: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.flowId, payload.eventId, payload.targetSubjectId); const groupProgress = state.groupProgress[id] as FlowProgress; - groupProgress.currentActivityId = action.payload.activityId; - groupProgress.pipelineActivityOrder = action.payload.pipelineActivityOrder; + groupProgress.currentActivityId = payload.activityId; + groupProgress.pipelineActivityOrder = payload.pipelineActivityOrder; groupProgress.currentActivityStartAt = new Date().getTime(); }, - entityCompleted: (state, action: PayloadAction) => { - const id = getProgressId(action.payload.entityId, action.payload.eventId); + entityCompleted: (state, { payload }: PayloadAction) => { + const id = getProgressId(payload.entityId, payload.eventId, payload.targetSubjectId); state.groupProgress[id].endAt = new Date().getTime(); @@ -290,18 +293,18 @@ const appletsSlice = createSlice({ const now = new Date().getTime(); - completedEntities[action.payload.entityId] = now; + completedEntities[payload.entityId] = now; - if (!completions[action.payload.entityId]) { - completions[action.payload.entityId] = {}; + if (!completions[payload.entityId]) { + completions[payload.entityId] = {}; } - const entityCompletions = completions[action.payload.entityId]; + const entityCompletions = completions[payload.entityId]; - if (!entityCompletions[action.payload.eventId]) { - entityCompletions[action.payload.eventId] = []; + if (!entityCompletions[payload.eventId]) { + entityCompletions[payload.eventId] = []; } - entityCompletions[action.payload.eventId].push(now); + entityCompletions[payload.eventId].push(now); }, initiateTakeNow: (state, action: PayloadAction) => { diff --git a/src/entities/applet/model/types.ts b/src/entities/applet/model/types.ts index 17222a2e5..395d96bb6 100644 --- a/src/entities/applet/model/types.ts +++ b/src/entities/applet/model/types.ts @@ -118,18 +118,21 @@ export type ProgressState = Record; export type SaveActivityProgressPayload = { activityId: string; eventId: string; + targetSubjectId: string | null; progress: ActivityProgress; }; export type ChangeSummaryScreenVisibilityPayload = { activityId: string; eventId: string; + targetSubjectId: string | null; isSummaryScreenOpen: boolean; }; export type SetItemTimerPayload = { activityId: string; eventId: string; + targetSubjectId: string | null; itemId: string; timerStatus: ItemTimerProgress; }; @@ -137,28 +140,33 @@ export type SetItemTimerPayload = { export type ItemTimerTickPayload = { activityId: string; eventId: string; + targetSubjectId: string | null; itemId: string; }; export type RemoveActivityProgressPayload = { activityId: string; eventId: string; + targetSubjectId: string | null; }; export type RemoveGroupProgressPayload = { entityId: string; eventId: string; + targetSubjectId: string | null; }; export type SaveGroupProgressPayload = { activityId: string; eventId: string; + targetSubjectId: string | null; progressPayload: GroupProgress; }; export type SaveSummaryDataInContext = { entityId: string; eventId: string; + targetSubjectId: string | null; activityId: string; summaryData: FlowSummaryData; @@ -167,6 +175,7 @@ export type SaveSummaryDataInContext = { export type SaveItemAnswerPayload = { entityId: string; eventId: string; + targetSubjectId: string | null; itemId: string; answer: Answer; }; @@ -174,6 +183,7 @@ export type SaveItemAnswerPayload = { export type SaveItemAdditionalTextPayload = { entityId: string; eventId: string; + targetSubjectId: string | null; itemId: string; additionalText: string; }; @@ -181,11 +191,13 @@ export type SaveItemAdditionalTextPayload = { export type UpdateStepPayload = { activityId: string; eventId: string; + targetSubjectId: string | null; }; export type SaveUserEventPayload = { entityId: string; eventId: string; + targetSubjectId: string | null; itemId: string; userEvent: UserEvent; }; @@ -193,6 +205,7 @@ export type SaveUserEventPayload = { export type UpdateUserEventByIndexPayload = { entityId: string; eventId: string; + targetSubjectId: string | null; userEventIndex: number; userEvent: UserEvent; }; @@ -208,17 +221,20 @@ export type CompletedEventEntities = Record; export type InProgressEntity = { entityId: string; eventId: string; + targetSubjectId: string | null; }; export type InProgressActivity = { activityId: string; eventId: string; + targetSubjectId: string | null; }; export type InProgressFlow = { flowId: string; activityId: string; eventId: string; + targetSubjectId: string | null; pipelineActivityOrder: number; }; diff --git a/src/entities/subject/api/useSubjectQuery.ts b/src/entities/subject/api/useSubjectQuery.ts index dda864f24..63d0d8db5 100644 --- a/src/entities/subject/api/useSubjectQuery.ts +++ b/src/entities/subject/api/useSubjectQuery.ts @@ -4,12 +4,12 @@ type FetchFn = typeof subjectService.getSubjectById; type Options = QueryOptions; export const useSubjectQuery = >( - subjectId: string, + subjectId: string | null, options?: Options, ) => { return useBaseQuery( ['subjectDetails', { subjectId }], - () => subjectService.getSubjectById({ subjectId }), + () => subjectService.getSubjectById({ subjectId: String(subjectId) }), options, ); }; diff --git a/src/features/AutoCompletion/model/hooks/useAutoCompletionRecord.ts b/src/features/AutoCompletion/model/hooks/useAutoCompletionRecord.ts index c81d3a0d3..0eba10f6d 100644 --- a/src/features/AutoCompletion/model/hooks/useAutoCompletionRecord.ts +++ b/src/features/AutoCompletion/model/hooks/useAutoCompletionRecord.ts @@ -7,11 +7,16 @@ import { useAppSelector } from '~/shared/utils'; type Props = { entityId: string; eventId: string; + targetSubjectId: string | null; }; -export const useAutoCompletionRecord = ({ entityId, eventId }: Props): AutoCompletion | null => { +export const useAutoCompletionRecord = ({ + entityId, + eventId, + targetSubjectId, +}: Props): AutoCompletion | null => { const state = useAppSelector((state) => - selectAutoCompletionRecord(state, getProgressId(entityId, eventId)), + selectAutoCompletionRecord(state, getProgressId(entityId, eventId, targetSubjectId)), ); return state ?? null; diff --git a/src/features/AutoCompletion/model/hooks/useAutoCompletionStateManager.ts b/src/features/AutoCompletion/model/hooks/useAutoCompletionStateManager.ts index 3c1301bfb..0adbe0414 100644 --- a/src/features/AutoCompletion/model/hooks/useAutoCompletionStateManager.ts +++ b/src/features/AutoCompletion/model/hooks/useAutoCompletionStateManager.ts @@ -7,6 +7,7 @@ import { useAppDispatch } from '~/shared/utils'; export type ActivitySuccessfullySubmitted = { entityId: string; eventId: string; + targetSubjectId: string | null; activityId: string; }; @@ -26,7 +27,7 @@ export const useAutoCompletionStateManager = () => { ); const removeAutoCompletion = useCallback( - (payload: { entityId: string; eventId: string }) => { + (payload: { entityId: string; eventId: string; targetSubjectId: string | null }) => { dispatch(actions.removeAutoCompletion(payload)); }, [dispatch], diff --git a/src/features/AutoCompletion/model/slice.ts b/src/features/AutoCompletion/model/slice.ts index 123d19c82..7beb700f8 100644 --- a/src/features/AutoCompletion/model/slice.ts +++ b/src/features/AutoCompletion/model/slice.ts @@ -5,6 +5,7 @@ import { getProgressId } from '~/abstract/lib'; type DefaultProps = { entityId: string; eventId: string; + targetSubjectId: string | null; }; export type SetAutoCompletionPayload = DefaultProps & { @@ -32,8 +33,8 @@ const slice = createSlice({ }, setAutoCompletion(state, action: PayloadAction) { - const { entityId, eventId, autoCompletion } = action.payload; - const progressId = getProgressId(entityId, eventId); + const { entityId, eventId, targetSubjectId, autoCompletion } = action.payload; + const progressId = getProgressId(entityId, eventId, targetSubjectId); const record = state[progressId]; @@ -43,8 +44,8 @@ const slice = createSlice({ }, removeAutoCompletion(state, action: PayloadAction) { - const { entityId, eventId } = action.payload; - const progressId = getProgressId(entityId, eventId); + const { entityId, eventId, targetSubjectId } = action.payload; + const progressId = getProgressId(entityId, eventId, targetSubjectId); delete state[progressId]; }, @@ -53,8 +54,8 @@ const slice = createSlice({ state, action: PayloadAction, ) { - const { entityId, eventId, activityId } = action.payload; - const progressId = getProgressId(entityId, eventId); + const { entityId, eventId, activityId, targetSubjectId } = action.payload; + const progressId = getProgressId(entityId, eventId, targetSubjectId); state[progressId].successfullySubmittedActivityIds.push(activityId); }, diff --git a/src/features/PassSurvey/hooks/useAnswers.ts b/src/features/PassSurvey/hooks/useAnswers.ts index 355b8ff28..46cbb68f0 100644 --- a/src/features/PassSurvey/hooks/useAnswers.ts +++ b/src/features/PassSurvey/hooks/useAnswers.ts @@ -31,6 +31,7 @@ export const useAnswerBuilder = (): AnswerBuilder => { const groupProgress = appletModel.hooks.useGroupProgressRecord({ entityId: context.entityId, eventId: context.eventId, + targetSubjectId: context.targetSubject?.id ?? null, }); const { getMultiInformantState, isInMultiInformantFlow } = @@ -73,21 +74,26 @@ export const useAnswerBuilder = (): AnswerBuilder => { const multiInformantState = getMultiInformantState(); if (isInMultiInformantFlow()) { + // Take Now flow answer.sourceSubjectId = multiInformantState?.sourceSubject?.id; answer.targetSubjectId = multiInformantState?.targetSubject?.id; + } else if (context.targetSubject) { + // Activity assignment + answer.targetSubjectId = context.targetSubject.id; } return answer; }, [ groupProgress, + context.encryption, context.event, context.appletId, context.appletVersion, context.flow, - context.encryption, context.publicAppletKey, context.integrations, + context.targetSubject, getMultiInformantState, isInMultiInformantFlow, ], diff --git a/src/features/PassSurvey/hooks/useItemTimer.ts b/src/features/PassSurvey/hooks/useItemTimer.ts index 369208b47..4742b70e9 100644 --- a/src/features/PassSurvey/hooks/useItemTimer.ts +++ b/src/features/PassSurvey/hooks/useItemTimer.ts @@ -11,6 +11,7 @@ const ONE_SEC = 1000; type Props = { activityId: string; eventId: string; + targetSubjectId: string | null; item: appletModel.ItemRecord; isSubmitModalOpen: boolean; onTimerEnd: () => void; @@ -27,6 +28,7 @@ export const useItemTimer = ({ onTimerEnd, activityId, eventId, + targetSubjectId, isSubmitModalOpen, }: Props): TimerSettings => { const prevItem = usePrevious(item); @@ -35,6 +37,7 @@ export const useItemTimer = ({ appletModel.hooks.useItemTimerState({ activityId, eventId, + targetSubjectId, itemId: item.id, }); diff --git a/src/features/PassSurvey/hooks/useStartSurvey.ts b/src/features/PassSurvey/hooks/useStartSurvey.ts index 66332f929..be8747547 100644 --- a/src/features/PassSurvey/hooks/useStartSurvey.ts +++ b/src/features/PassSurvey/hooks/useStartSurvey.ts @@ -13,6 +13,7 @@ import { type NavigateToEntityProps = { flowId: string | null; activityId: string; + targetSubjectId: string | null; entityType: EntityType; eventId: string; }; @@ -20,6 +21,7 @@ type NavigateToEntityProps = { type OnActivityCardClickProps = { activityId: string; eventId: string; + targetSubjectId: string | null; flowId: string | null; status: ActivityStatus; shouldRestart: boolean; @@ -45,7 +47,7 @@ export const useStartSurvey = (props: Props) => { appletModel.hooks.useMultiInformantState(); function navigateToEntity(params: NavigateToEntityProps) { - const { activityId, flowId, eventId, entityType } = params; + const { activityId, flowId, eventId, targetSubjectId, entityType } = params; if (props.isPublic && props.publicAppletKey) { return navigator.navigate( @@ -65,13 +67,20 @@ export const useStartSurvey = (props: Props) => { appletId, activityId, eventId, + targetSubjectId, entityType, flowId, }), ); } - function startSurvey({ activityId, flowId, eventId, shouldRestart }: OnActivityCardClickProps) { + function startSurvey({ + activityId, + flowId, + eventId, + targetSubjectId, + shouldRestart, + }: OnActivityCardClickProps) { const analyticsPayload: MixpanelPayload = { [MixpanelProps.AppletId]: props.applet.id, [MixpanelProps.ActivityId]: activityId, @@ -103,8 +112,8 @@ export const useStartSurvey = (props: Props) => { } if (shouldRestart) { - removeActivityProgress({ activityId, eventId }); - removeGroupProgress({ entityId: flowId, eventId }); + removeActivityProgress({ activityId, eventId, targetSubjectId }); + removeGroupProgress({ entityId: flowId, eventId, targetSubjectId }); } const activityIdToNavigate = shouldRestart ? firstActivityId : activityId; @@ -114,12 +123,13 @@ export const useStartSurvey = (props: Props) => { entityType: 'flow', eventId, flowId, + targetSubjectId, }); } if (shouldRestart) { - removeActivityProgress({ activityId, eventId }); - removeGroupProgress({ entityId: activityId, eventId }); + removeActivityProgress({ activityId, eventId, targetSubjectId }); + removeGroupProgress({ entityId: activityId, eventId, targetSubjectId }); } return navigateToEntity({ @@ -127,6 +137,7 @@ export const useStartSurvey = (props: Props) => { entityType: 'regular', eventId, flowId: null, + targetSubjectId, }); } diff --git a/src/features/PassSurvey/hooks/useSummaryData.ts b/src/features/PassSurvey/hooks/useSummaryData.ts index f97c6a47f..9d211563c 100644 --- a/src/features/PassSurvey/hooks/useSummaryData.ts +++ b/src/features/PassSurvey/hooks/useSummaryData.ts @@ -12,6 +12,7 @@ type Props = { activityName: string; eventId: string; flowId: string | null; + targetSubjectId: string | null; scoresAndReports?: ScoreAndReports; }; @@ -36,9 +37,10 @@ export const useSummaryData = (props: Props) => { const groupProgress = appletModel.hooks.useGroupProgressRecord({ entityId: props.flowId ? props.flowId : props.activityId, eventId: props.eventId, + targetSubjectId: props.targetSubjectId, }); - const progressId = getProgressId(props.activityId, props.eventId); + const progressId = getProgressId(props.activityId, props.eventId, props.targetSubjectId); const activityProgress = useAppSelector((state) => appletModel.selectors.selectActivityProgress(state, progressId), diff --git a/src/features/PassSurvey/hooks/useSurveyDataQuery.ts b/src/features/PassSurvey/hooks/useSurveyDataQuery.ts index 58c96b494..8b11e7c29 100644 --- a/src/features/PassSurvey/hooks/useSurveyDataQuery.ts +++ b/src/features/PassSurvey/hooks/useSurveyDataQuery.ts @@ -1,6 +1,9 @@ +import { UseQueryResult } from '@tanstack/react-query'; + import { useActivityByIdQuery } from '~/entities/activity'; import { useAppletByIdQuery } from '~/entities/applet'; import { useEventsbyAppletIdQuery } from '~/entities/event'; +import { useSubjectQuery } from '~/entities/subject'; import { ActivityDTO, AppletDTO, @@ -8,12 +11,15 @@ import { BaseError, RespondentMetaDTO, } from '~/shared/api'; +import { SubjectDTO } from '~/shared/api/types/subject'; +import { useFeatureFlags } from '~/shared/utils'; type Return = { appletDTO: AppletDTO | null; respondentMeta?: RespondentMetaDTO; activityDTO: ActivityDTO | null; eventsDTO: AppletEventsResponse | null; + targetSubject: SubjectDTO | null; isError: boolean; isLoading: boolean; error: BaseError | null; @@ -23,10 +29,13 @@ type Props = { publicAppletKey: string | null; appletId: string; activityId: string; + targetSubjectId: string | null; }; export const useSurveyDataQuery = (props: Props): Return => { - const { appletId, activityId, publicAppletKey } = props; + const { appletId, activityId, publicAppletKey, targetSubjectId } = props; + const { featureFlags } = useFeatureFlags(); + const isAssignmentsEnabled = !!featureFlags?.enableActivityAssign && !!targetSubjectId; const { data: appletById, @@ -58,13 +67,26 @@ export const useSurveyDataQuery = (props: Props): Return => { { select: (data) => data?.data?.result }, ); + const subjectQueryResult = useSubjectQuery(targetSubjectId, { + select: (data) => data.data.result, + enabled: isAssignmentsEnabled, + }); + + const { + data: targetSubject, + isError: isSubjectError, + isLoading: isSubjectLoading, + error: subjectError, + } = isAssignmentsEnabled ? subjectQueryResult : ({} as UseQueryResult); + return { appletDTO: appletById?.result ?? null, respondentMeta: appletById?.respondentMeta, activityDTO: activityById ?? null, eventsDTO: eventsByIdData ?? null, - isError: isAppletError || isActivityError || isEventsError, - isLoading: isAppletLoading || isActivityLoading || isEventsLoading, - error: appletError ?? activityError ?? eventsError, + targetSubject: targetSubject ?? null, + isError: isAppletError || isActivityError || isEventsError || isSubjectError, + isLoading: isAppletLoading || isActivityLoading || isEventsLoading || isSubjectLoading, + error: appletError ?? activityError ?? eventsError ?? subjectError, }; }; diff --git a/src/features/PassSurvey/lib/SurveyContext.ts b/src/features/PassSurvey/lib/SurveyContext.ts index 5b72041bb..9511d3a71 100644 --- a/src/features/PassSurvey/lib/SurveyContext.ts +++ b/src/features/PassSurvey/lib/SurveyContext.ts @@ -7,6 +7,7 @@ import { RespondentMetaDTO, ScheduleEventDto, } from '~/shared/api'; +import { SubjectDTO } from '~/shared/api/types/subject'; export type SurveyContext = { appletId: string; @@ -15,6 +16,7 @@ export type SurveyContext = { activityId: string; eventId: string; + targetSubject: SubjectDTO | null; entityId: string; diff --git a/src/features/PassSurvey/lib/mappers.ts b/src/features/PassSurvey/lib/mappers.ts index 4fe869b74..e598f6295 100644 --- a/src/features/PassSurvey/lib/mappers.ts +++ b/src/features/PassSurvey/lib/mappers.ts @@ -7,12 +7,14 @@ import { AppletEventsResponse, RespondentMetaDTO, } from '~/shared/api'; +import { SubjectDTO } from '~/shared/api/types/subject'; type Props = { appletDTO: AppletDTO | null; eventsDTO: AppletEventsResponse | null; respondentMeta?: RespondentMetaDTO; activityDTO: ActivityDTO | null; + targetSubject: SubjectDTO | null; currentEventId: string; flowId: string | null; @@ -20,7 +22,7 @@ type Props = { }; export const mapRawDataToSurveyContext = (props: Props): SurveyContext => { - const { appletDTO, eventsDTO, activityDTO, currentEventId, flowId } = props; + const { appletDTO, eventsDTO, activityDTO, currentEventId, flowId, targetSubject } = props; if (!appletDTO || !eventsDTO || !activityDTO) { throw new Error('[MapRawDataToSurveyContext] Missing required data'); @@ -51,6 +53,7 @@ export const mapRawDataToSurveyContext = (props: Props): SurveyContext => { activity: activityDTO, event, + targetSubject, respondentMeta: props.respondentMeta, encryption: appletDTO.encryption, diff --git a/src/features/PassSurvey/ui/EntityTimer.tsx b/src/features/PassSurvey/ui/EntityTimer.tsx index 0fe939a40..b3c426e44 100644 --- a/src/features/PassSurvey/ui/EntityTimer.tsx +++ b/src/features/PassSurvey/ui/EntityTimer.tsx @@ -29,7 +29,7 @@ export const EntityTimer = ({ entityTimerSettings }: Props) => { const group = useAppSelector((state) => appletModel.selectors.selectGroupProgress( state, - getProgressId(context.entityId, context.eventId), + getProgressId(context.entityId, context.eventId, context.targetSubject?.id), ), ); diff --git a/src/pages/AutoCompletion/index.tsx b/src/pages/AutoCompletion/index.tsx index 348c29e76..b93be0caa 100644 --- a/src/pages/AutoCompletion/index.tsx +++ b/src/pages/AutoCompletion/index.tsx @@ -9,6 +9,7 @@ function AutoCompletion() { const activityId = searchParams.get('activityId'); const eventId = searchParams.get('eventId'); const flowId = searchParams.get('flowId'); + const targetSubjectId = searchParams.get('targetSubjectId'); if (!appletId || !activityId || !eventId) { return
Invalid URL
; @@ -20,6 +21,7 @@ function AutoCompletion() { activityId={activityId} eventId={eventId} flowId={flowId} + targetSubjectId={targetSubjectId} publicAppletKey={null} /> ); diff --git a/src/pages/PublicAutoCompletion/index.tsx b/src/pages/PublicAutoCompletion/index.tsx index ae636747b..c39eb8ba9 100644 --- a/src/pages/PublicAutoCompletion/index.tsx +++ b/src/pages/PublicAutoCompletion/index.tsx @@ -21,6 +21,7 @@ function PublicAutoCompletion() { activityId={activityId} eventId={eventId} flowId={flowId} + targetSubjectId={null} publicAppletKey={publicAppletKey} /> ); diff --git a/src/pages/PublicSurvey/index.tsx b/src/pages/PublicSurvey/index.tsx index 26155f77a..ed9391b56 100644 --- a/src/pages/PublicSurvey/index.tsx +++ b/src/pages/PublicSurvey/index.tsx @@ -30,6 +30,7 @@ function PublicSurvey() { activityId={activityId} eventId={eventId} flowId={flowId} + targetSubjectId={null} publicAppletKey={publicAppletKey} /> diff --git a/src/pages/Survey/index.tsx b/src/pages/Survey/index.tsx index 831bc2f1a..461d6e58a 100644 --- a/src/pages/Survey/index.tsx +++ b/src/pages/Survey/index.tsx @@ -12,12 +12,8 @@ function SurveyPage() { const [searchParams] = useSearchParams(); const isFlow = entityType === 'flow'; - - let flowId: string | null = null; - - if (isFlow) { - flowId = searchParams.get('flowId'); - } + const flowId = isFlow ? searchParams.get('flowId') : null; + const targetSubjectId = searchParams.get('targetSubjectId'); if (!appletId || !activityId || !eventId) { return
{t('wrondLinkParametrError')}
; @@ -30,6 +26,7 @@ function SurveyPage() { activityId={activityId} eventId={eventId} flowId={flowId} + targetSubjectId={targetSubjectId} publicAppletKey={null} /> diff --git a/src/shared/api/types/activity.ts b/src/shared/api/types/activity.ts index 32354f88c..67d711bb8 100644 --- a/src/shared/api/types/activity.ts +++ b/src/shared/api/types/activity.ts @@ -222,6 +222,7 @@ export type CompletedEntityDTO = { id: string; answerId: string; submitId: string; + targetSubjectId: string | null; scheduledEventId: string; localEndDate: string; localEndTime: string; diff --git a/src/shared/constants/routes.ts b/src/shared/constants/routes.ts index 0af64c084..dec08e833 100644 --- a/src/shared/constants/routes.ts +++ b/src/shared/constants/routes.ts @@ -86,16 +86,21 @@ const ROUTES = { entityType, eventId, flowId, + targetSubjectId, }: { appletId: string; activityId: string; eventId: string; entityType: 'regular' | 'flow'; flowId: string | null; - }) => - `/protected/applets/${appletId}/activityId/${activityId}/event/${eventId}/entityType/${entityType}?${ - flowId ? `flowId=${flowId}` : '' - }`, + targetSubjectId: string | null; + }) => { + const params = new URLSearchParams(); + if (flowId) params.append('flowId', flowId); + if (targetSubjectId) params.append('targetSubjectId', targetSubjectId); + + return `/protected/applets/${appletId}/activityId/${activityId}/event/${eventId}/entityType/${entityType}?${params.toString()}`; + }, }, invitationAccept: { path: '/protected/invite/accepted', @@ -112,14 +117,25 @@ const ROUTES = { activityId, flowId, publicAppletKey, + targetSubjectId, }: { appletId: string; activityId: string; eventId: string; flowId: string | null; publicAppletKey: string | null; - }) => - `${ROUTES.autoCompletion.path}?appletId=${appletId}&eventId=${eventId}&activityId=${activityId}${flowId ? `&flowId=${flowId}` : ''}${publicAppletKey ? `&publicAppletKey=${publicAppletKey}` : ''}`, + targetSubjectId: string | null; + }) => { + const params = new URLSearchParams(); + params.append('appletId', appletId); + params.append('eventId', eventId); + params.append('activityId', activityId); + if (flowId) params.append('flowId', flowId); + if (publicAppletKey) params.append('publicAppletKey', publicAppletKey); + if (targetSubjectId) params.append('targetSubjectId', targetSubjectId); + + return `${ROUTES.autoCompletion.path}?${params.toString()}`; + }, }, }; diff --git a/src/shared/utils/hooks/index.ts b/src/shared/utils/hooks/index.ts index c2d91d36a..d1ca4c2f3 100644 --- a/src/shared/utils/hooks/index.ts +++ b/src/shared/utils/hooks/index.ts @@ -12,3 +12,4 @@ export * from './useForceUpdate'; export * from './usePrevious'; export * from './useIntersectionObserver'; export * from './useWindowFocus'; +export * from './useFeatureFlags'; diff --git a/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts b/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts index 45ecd79f7..47494eb33 100644 --- a/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts +++ b/src/widgets/ActivityGroups/lib/AppletDetailsContext.ts @@ -5,8 +5,8 @@ import { AppletBaseDTO, AppletEventsResponse, MyAssignmentsDTO } from '~/shared/ type AppletDetailsContextProps = { applet: AppletBaseDTO; events: AppletEventsResponse; - /** Activity assignments array - undefined if activity assign is disabled */ - assignments?: MyAssignmentsDTO['assignments']; + /** Activity assignments array - null if activity assign is disabled */ + assignments: MyAssignmentsDTO['assignments'] | null; }; type Public = { diff --git a/src/widgets/ActivityGroups/model/hooks/useActivityGroups.ts b/src/widgets/ActivityGroups/model/hooks/useActivityGroups.ts index a800c9f63..d543a079a 100644 --- a/src/widgets/ActivityGroups/model/hooks/useActivityGroups.ts +++ b/src/widgets/ActivityGroups/model/hooks/useActivityGroups.ts @@ -8,7 +8,7 @@ import { useAppSelector } from '~/shared/utils'; type Props = { applet: AppletBaseDTO; events: AppletEventsResponse; - assignments?: HydratedAssignmentDTO[]; + assignments: HydratedAssignmentDTO[] | null; }; type Return = { diff --git a/src/widgets/ActivityGroups/model/hooks/useEntitiesSync.ts b/src/widgets/ActivityGroups/model/hooks/useEntitiesSync.ts index 644f16dcd..a8ab2c511 100644 --- a/src/widgets/ActivityGroups/model/hooks/useEntitiesSync.ts +++ b/src/widgets/ActivityGroups/model/hooks/useEntitiesSync.ts @@ -21,16 +21,19 @@ export const useEntitiesSync = (props: FilterCompletedEntitiesProps) => { const entityId = entity.id; const eventId = entity.scheduledEventId; + const targetSubjectId = entity.targetSubjectId; const groupProgress = getGroupProgress({ entityId, eventId, + targetSubjectId, }); if (!groupProgress) { return saveGroupProgress({ activityId: entityId, eventId, + targetSubjectId, progressPayload: { type: ActivityPipelineType.Regular, startAt: null, @@ -52,6 +55,7 @@ export const useEntitiesSync = (props: FilterCompletedEntitiesProps) => { return saveGroupProgress({ activityId: entityId, eventId, + targetSubjectId, progressPayload: { ...groupProgress, endAt: new Date(endAtDate).getTime(), diff --git a/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts b/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts index 0c9b49af4..2f3eab915 100644 --- a/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts +++ b/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts @@ -24,7 +24,7 @@ type BuildResult = { type ProcessParams = { activities: ActivityBaseDTO[]; flows: ActivityFlowDTO[]; - assignments: HydratedAssignmentDTO[] | undefined; + assignments: HydratedAssignmentDTO[] | null; events: AppletEventsResponse; entityProgress: GroupProgressState; }; @@ -116,7 +116,7 @@ const createActivityGroupsBuildManager = () => { // Add auto-assignment if enabled for this activity/flow if (entity.autoAssign) { - eventEntities.push({ entity, event }); + eventEntities.push({ entity, event, targetSubject: null }); } // Add any additional assignments (without duplicating possible auto-assignment) @@ -124,13 +124,18 @@ const createActivityGroupsBuildManager = () => { const isSelfAssign = respondentSubject.id === targetSubject.id; if (entity.autoAssign && isSelfAssign) continue; - eventEntities.push(isSelfAssign ? { entity, event } : { entity, event, targetSubject }); + eventEntities.push({ + entity, + event, + targetSubject: isSelfAssign ? null : targetSubject, + }); } } else { // Assignments disabled eventEntities.push({ entity, event, + targetSubject: null, }); } } diff --git a/src/widgets/ActivityGroups/ui/ActivityCard/index.tsx b/src/widgets/ActivityGroups/ui/ActivityCard/index.tsx index 64c5bba85..428a27a3e 100644 --- a/src/widgets/ActivityGroups/ui/ActivityCard/index.tsx +++ b/src/widgets/ActivityGroups/ui/ActivityCard/index.tsx @@ -51,7 +51,11 @@ export const ActivityCard = ({ activityListItem }: Props) => { applet: context.applet, }); - const activityEventId = getProgressId(activityListItem.activityId, activityListItem.eventId); + const activityEventId = getProgressId( + activityListItem.activityId, + activityListItem.eventId, + activityListItem.targetSubject?.id, + ); const activityProgress = useAppSelector((state) => appletModel.selectors.selectActivityProgress(state, activityEventId), @@ -60,6 +64,7 @@ export const ActivityCard = ({ activityListItem }: Props) => { const autoCompletionRecord = useAutoCompletionRecord({ entityId: activityListItem.flowId ?? activityListItem.activityId, eventId: activityListItem.eventId, + targetSubjectId: activityListItem.targetSubject?.id ?? null, }); const step = activityProgress?.step || 0; @@ -101,6 +106,7 @@ export const ActivityCard = ({ activityListItem }: Props) => { return startSurvey({ activityId: activityListItem.activityId, eventId: activityListItem.eventId, + targetSubjectId: activityListItem.targetSubject?.id ?? null, status: activityListItem.status, flowId: activityListItem.flowId, shouldRestart, diff --git a/src/widgets/ActivityGroups/ui/index.tsx b/src/widgets/ActivityGroups/ui/index.tsx index 7e6faa558..38355db71 100644 --- a/src/widgets/ActivityGroups/ui/index.tsx +++ b/src/widgets/ActivityGroups/ui/index.tsx @@ -13,7 +13,7 @@ import { TakeNowSuccessModal } from '~/features/TakeNow/ui/TakeNowSuccessModal'; import Box from '~/shared/ui/Box'; import Loader from '~/shared/ui/Loader'; import { useCustomTranslation, useOnceEffect } from '~/shared/utils'; -import { useFeatureFlags } from '~/shared/utils/hooks/useFeatureFlags'; +import { useFeatureFlags } from '~/shared/utils/hooks'; type PublicAppletDetails = { isPublic: true; @@ -57,7 +57,7 @@ export const ActivityGroups = (props: Props) => { const { isError: isAssignmentsError, isLoading: isAssignmentsLoading, - data: assignments, + data: assignments = null, } = useMyAssignmentsQuery(isAssignmentsEnabled ? props.appletId : undefined, { select: (data) => data.data.result.assignments, enabled: isAssignmentsEnabled, diff --git a/src/widgets/AutoCompletion/lib/useAutoCompletion.ts b/src/widgets/AutoCompletion/lib/useAutoCompletion.ts index 0244d9cbb..3ac9bce38 100644 --- a/src/widgets/AutoCompletion/lib/useAutoCompletion.ts +++ b/src/widgets/AutoCompletion/lib/useAutoCompletion.ts @@ -15,6 +15,7 @@ export const useAutoCompletion = () => { const completionState = AutoCompletionModel.useAutoCompletionRecord({ entityId: context.entityId, eventId: context.eventId, + targetSubjectId: context.targetSubject?.id ?? null, }); const { activitySuccessfullySubmitted } = AutoCompletionModel.useAutoCompletionStateManager(); @@ -50,6 +51,7 @@ export const useAutoCompletion = () => { activityProgress: getActivityProgress({ activityId: context.activityId, eventId: context.eventId, + targetSubjectId: context.targetSubject?.id ?? null, }), answerBuilder, }, @@ -72,6 +74,7 @@ export const useAutoCompletion = () => { activitySuccessfullySubmitted({ entityId: context.entityId, eventId: context.eventId, + targetSubjectId: context.targetSubject?.id ?? null, activityId, }); } @@ -88,6 +91,7 @@ export const useAutoCompletion = () => { context.entityId, context.eventId, context.publicAppletKey, + context.targetSubject?.id, getActivityProgress, submitAnswersAsync, ]); diff --git a/src/widgets/AutoCompletion/ui/AutoCompletionScreen.tsx b/src/widgets/AutoCompletion/ui/AutoCompletionScreen.tsx index ad774e7a6..ad9ef3d33 100644 --- a/src/widgets/AutoCompletion/ui/AutoCompletionScreen.tsx +++ b/src/widgets/AutoCompletion/ui/AutoCompletionScreen.tsx @@ -45,16 +45,19 @@ export const AutoCompletionScreen = () => { removeAutoCompletion({ entityId: context.entityId, eventId: context.eventId, + targetSubjectId: context.targetSubject?.id ?? null, }); entityCompleted({ entityId: context.entityId, eventId: context.eventId, + targetSubjectId: context.targetSubject?.id ?? null, }); removeActivityProgress({ activityId: context.activityId, eventId: context.eventId, + targetSubjectId: context.targetSubject?.id ?? null, }); if (context.publicAppletKey) { @@ -74,6 +77,7 @@ export const AutoCompletionScreen = () => { context.entityId, context.eventId, context.publicAppletKey, + context.targetSubject?.id, entityCompleted, navigator, removeActivityProgress, diff --git a/src/widgets/AutoCompletion/ui/index.tsx b/src/widgets/AutoCompletion/ui/index.tsx index 3693f5d7a..0ff6e319f 100644 --- a/src/widgets/AutoCompletion/ui/index.tsx +++ b/src/widgets/AutoCompletion/ui/index.tsx @@ -11,18 +11,28 @@ type Props = { appletId: string; activityId: string; eventId: string; + targetSubjectId: string | null; flowId: string | null; publicAppletKey: string | null; }; function SurveyAutoCompletionWidget(props: Props) { - const { appletDTO, activityDTO, eventsDTO, isLoading, isError, error, respondentMeta } = - useSurveyDataQuery({ - appletId: props.appletId, - activityId: props.activityId, - publicAppletKey: props.publicAppletKey, - }); + const { + appletDTO, + respondentMeta, + activityDTO, + eventsDTO, + targetSubject, + isError, + isLoading, + error, + } = useSurveyDataQuery({ + appletId: props.appletId, + activityId: props.activityId, + publicAppletKey: props.publicAppletKey, + targetSubjectId: props.targetSubjectId, + }); if (isLoading) { return ; @@ -40,6 +50,7 @@ function SurveyAutoCompletionWidget(props: Props) { eventsDTO, currentEventId: props.eventId, flowId: props.flowId, + targetSubject, publicAppletKey: props.publicAppletKey, respondentMeta, })} diff --git a/src/widgets/Survey/model/hooks/useEntityTimer.ts b/src/widgets/Survey/model/hooks/useEntityTimer.ts index 1e98e3eb9..351ae22a5 100644 --- a/src/widgets/Survey/model/hooks/useEntityTimer.ts +++ b/src/widgets/Survey/model/hooks/useEntityTimer.ts @@ -15,12 +15,13 @@ export const useEntityTimer = ({ onFinish }: Props) => { const groupProgress = appletModel.hooks.useGroupProgressRecord({ entityId: context.entityId, eventId: context.eventId, + targetSubjectId: context.targetSubject?.id ?? null, }); const activityProgress = useAppSelector((state) => appletModel.selectors.selectActivityProgress( state, - getProgressId(context.activityId, context.eventId), + getProgressId(context.activityId, context.eventId, context.targetSubject?.id), ), ); diff --git a/src/widgets/Survey/ui/PassingScreen.tsx b/src/widgets/Survey/ui/PassingScreen.tsx index f3b2c60ff..855f78417 100644 --- a/src/widgets/Survey/ui/PassingScreen.tsx +++ b/src/widgets/Survey/ui/PassingScreen.tsx @@ -36,21 +36,25 @@ const PassingScreen = (props: Props) => { const context = useContext(SurveyContext); + const targetSubjectId = context.targetSubject?.id ?? null; + const activityProgress = useAppSelector((state) => appletModel.selectors.selectActivityProgress( state, - getProgressId(context.activityId, context.eventId), + getProgressId(context.activityId, context.eventId, context.targetSubject?.id), ), ); const groupProgress = appletModel.hooks.useGroupProgressRecord({ entityId: context.entityId, eventId: context.eventId, + targetSubjectId, }); const autoCompletionState = AutoCompletionModel.useAutoCompletionRecord({ entityId: context.entityId, eventId: context.eventId, + targetSubjectId, }); const { saveSummaryDataInContext } = appletModel.hooks.useGroupProgressStateManager(); @@ -66,18 +70,21 @@ const PassingScreen = (props: Props) => { appletModel.hooks.useUserEvents({ activityId: context.activityId, eventId: context.eventId, + targetSubjectId, }); const { saveItemAnswer, saveItemAdditionalText, removeItemAnswer } = appletModel.hooks.useSaveItemAnswer({ activityId: context.activityId, eventId: context.eventId, + targetSubjectId, }); const { getSummaryForCurrentActivity } = useSummaryData({ activityId: context.activityId, activityName: context.activity.name, eventId: context.eventId, + targetSubjectId, scoresAndReports: context.activity.scoresAndReports, flowId: null, }); @@ -92,6 +99,7 @@ const PassingScreen = (props: Props) => { const { completeActivity, completeFlow } = appletModel.hooks.useEntityComplete({ activityId: context.activityId, eventId: context.eventId, + targetSubjectId, publicAppletKey: context.publicAppletKey, flowId: context.flow?.id ?? null, appletId: context.appletId, @@ -141,6 +149,7 @@ const PassingScreen = (props: Props) => { saveSummaryDataInContext({ entityId: context.entityId, eventId: context.eventId, + targetSubjectId, activityId: context.activityId, summaryData: summaryDataContext, @@ -160,14 +169,22 @@ const PassingScreen = (props: Props) => { if (isFlowGroup && context.flow) { if (isLastActivity && hasAnySummaryScreenResults) { - return openSummaryScreen({ activityId: context.activityId, eventId: context.eventId }); + return openSummaryScreen({ + activityId: context.activityId, + eventId: context.eventId, + targetSubjectId, + }); } return completeFlow(); } if (isSummaryScreenOn && isSummaryDataExist) { - return openSummaryScreen({ activityId: context.activityId, eventId: context.eventId }); + return openSummaryScreen({ + activityId: context.activityId, + eventId: context.eventId, + targetSubjectId, + }); } return completeActivity(); @@ -202,13 +219,18 @@ const PassingScreen = (props: Props) => { saveUserEventByType('NEXT', item); } - return incrementStep({ activityId: context.activityId, eventId: context.eventId }); + return incrementStep({ + activityId: context.activityId, + eventId: context.eventId, + targetSubjectId, + }); }, [ item, context.activity.isSkippable, context.activityId, context.eventId, incrementStep, + targetSubjectId, saveUserEventByType, ]); @@ -219,8 +241,20 @@ const PassingScreen = (props: Props) => { return; } - return decrementStep({ activityId: context.activityId, eventId: context.eventId }); - }, [context.activityId, context.eventId, decrementStep, hasPrevStep, item, saveUserEventByType]); + return decrementStep({ + activityId: context.activityId, + eventId: context.eventId, + targetSubjectId, + }); + }, [ + context.activityId, + context.eventId, + decrementStep, + hasPrevStep, + item, + saveUserEventByType, + targetSubjectId, + ]); const onMoveForward = useCallback(() => { if (!item) { @@ -285,6 +319,7 @@ const PassingScreen = (props: Props) => { item, activityId: context.activityId, eventId: context.eventId, + targetSubjectId, isSubmitModalOpen, onTimerEnd: hasNextStep ? onNext : openSubmitModal, }); diff --git a/src/widgets/Survey/ui/ScreenManager.tsx b/src/widgets/Survey/ui/ScreenManager.tsx index a7981cfd0..34eb8226c 100644 --- a/src/widgets/Survey/ui/ScreenManager.tsx +++ b/src/widgets/Survey/ui/ScreenManager.tsx @@ -21,7 +21,7 @@ export const ScreenManager = ({ openTimesUpModal }: Props) => { const activityProgress = useAppSelector((state) => appletModel.selectors.selectActivityProgress( state, - getProgressId(context.activityId, context.eventId), + getProgressId(context.activityId, context.eventId, context.targetSubject?.id), ), ); @@ -38,6 +38,7 @@ export const ScreenManager = ({ openTimesUpModal }: Props) => { saveAutoCompletion({ entityId: context.entityId, eventId: context.eventId, + targetSubjectId: context.targetSubject?.id ?? null, autoCompletion: { activityIdsToSubmit: activitiesToSubmit, successfullySubmittedActivityIds: [], @@ -51,6 +52,7 @@ export const ScreenManager = ({ openTimesUpModal }: Props) => { context.entityId, context.eventId, context.flow, + context.targetSubject?.id, openTimesUpModal, saveAutoCompletion, ]); diff --git a/src/widgets/Survey/ui/SummaryScreen/index.tsx b/src/widgets/Survey/ui/SummaryScreen/index.tsx index 42eeecadb..15a9e2adb 100644 --- a/src/widgets/Survey/ui/SummaryScreen/index.tsx +++ b/src/widgets/Survey/ui/SummaryScreen/index.tsx @@ -27,13 +27,14 @@ const SummaryScreen = () => { const activityProgress = useAppSelector((state) => appletModel.selectors.selectActivityProgress( state, - getProgressId(context.activityId, context.eventId), + getProgressId(context.activityId, context.eventId, context.targetSubject?.id), ), ); const { completeActivity, completeFlow } = appletModel.hooks.useEntityComplete({ eventId: context.eventId, activityId: context.activityId, + targetSubjectId: context.targetSubject?.id ?? null, publicAppletKey: context.publicAppletKey, flowId: context.flow?.id ?? null, appletId: context.appletId, @@ -47,6 +48,7 @@ const SummaryScreen = () => { const { summaryData } = useSummaryData({ activityId: context.activityId, eventId: context.eventId, + targetSubjectId: context.targetSubject?.id ?? null, activityName: context.activity.name, scoresAndReports: activityProgress.scoreSettings, flowId: context.flow?.id ?? null, diff --git a/src/widgets/Survey/ui/WelcomeScreen.tsx b/src/widgets/Survey/ui/WelcomeScreen.tsx index 6599b9d36..f76597632 100644 --- a/src/widgets/Survey/ui/WelcomeScreen.tsx +++ b/src/widgets/Survey/ui/WelcomeScreen.tsx @@ -23,12 +23,13 @@ const WelcomeScreen = () => { const { setInitialProgress } = appletModel.hooks.useActivityProgress(); const isFlow = !!context.flow; - const isTimedActivity = !!context.event?.timers?.timer; + const targetSubjectId = context.targetSubject?.id ?? null; const groupProgress = appletModel.hooks.useGroupProgressRecord({ entityId: context.entityId, eventId: context.eventId, + targetSubjectId, }); const startAssessment = useCallback(() => { @@ -37,19 +38,24 @@ const WelcomeScreen = () => { const isGroupStarted = isGroupDefined && groupProgress.startAt && !groupProgress.endAt; if (context.flow && !isGroupStarted) { - startFlow(context.eventId, context.flow); + startFlow(context.eventId, context.flow, targetSubjectId); } if (!context.flow) { - startActivity(context.activityId, context.eventId); + startActivity(context.activityId, context.eventId, targetSubjectId); } - return setInitialProgress({ activity: context.activity, eventId: context.eventId }); + return setInitialProgress({ + activity: context.activity, + eventId: context.eventId, + targetSubjectId, + }); }, [ context.activity, context.activityId, context.eventId, context.flow, + targetSubjectId, groupProgress, setInitialProgress, startActivity, diff --git a/src/widgets/Survey/ui/index.tsx b/src/widgets/Survey/ui/index.tsx index 79dda728c..ff9dbc668 100644 --- a/src/widgets/Survey/ui/index.tsx +++ b/src/widgets/Survey/ui/index.tsx @@ -21,12 +21,13 @@ type Props = { appletId: string; activityId: string; eventId: string; + targetSubjectId: string | null; flowId: string | null; }; export const SurveyWidget = (props: Props) => { - const { publicAppletKey, appletId, activityId, eventId, flowId } = props; + const { publicAppletKey, appletId, activityId, eventId, flowId, targetSubjectId } = props; const { t } = useCustomTranslation(); const navigator = useCustomNavigation(); @@ -36,6 +37,7 @@ export const SurveyWidget = (props: Props) => { const autoCompletionState = AutoCompletionModel.useAutoCompletionRecord({ entityId: props.flowId ?? props.activityId, eventId: props.eventId, + targetSubjectId, }); const isTimesUpModalModalByDefault: boolean = @@ -60,6 +62,7 @@ export const SurveyWidget = (props: Props) => { activityId, flowId, eventId, + targetSubjectId, publicAppletKey, }; @@ -68,10 +71,18 @@ export const SurveyWidget = (props: Props) => { : ROUTES.autoCompletion.navigateTo(navigateToProps); return navigator.navigate(screenToNavigate); - }, [closeTimesUpModal, navigator, props]); - - const { activityDTO, appletDTO, eventsDTO, respondentMeta, isLoading, isError, error } = - useSurveyDataQuery({ publicAppletKey, appletId, activityId }); + }, [closeTimesUpModal, navigator, props, targetSubjectId]); + + const { + activityDTO, + appletDTO, + eventsDTO, + respondentMeta, + targetSubject, + isLoading, + isError, + error, + } = useSurveyDataQuery({ publicAppletKey, appletId, activityId, targetSubjectId }); if (isLoading) { return ; @@ -108,6 +119,7 @@ export const SurveyWidget = (props: Props) => { respondentMeta, currentEventId: eventId, flowId, + targetSubject, publicAppletKey, })} > From 8f14baf4ef417701ed9898ccbadf10f9236394f4 Mon Sep 17 00:00:00 2001 From: farmerpaul Date: Fri, 30 Aug 2024 13:10:27 -0300 Subject: [PATCH 7/9] test: fix existing tests --- .../ActivityGroupsBuilder.test.ts | 51 ++++++++----------- .../AvailableGroupBuilder.test.ts | 2 + .../ScheduledGroupEvaluator.test.ts | 2 + 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/src/abstract/lib/GroupBuilder/ActivityGroupsBuilder.test.ts b/src/abstract/lib/GroupBuilder/ActivityGroupsBuilder.test.ts index 73607c090..9ee45f07a 100644 --- a/src/abstract/lib/GroupBuilder/ActivityGroupsBuilder.test.ts +++ b/src/abstract/lib/GroupBuilder/ActivityGroupsBuilder.test.ts @@ -50,6 +50,7 @@ const getActivity = (): Entity => { order: 0, type: ActivityType.NotDefined, image: null, + autoAssign: true, }; return result; }; @@ -98,6 +99,7 @@ const getExpectedItem = (): ActivityListItem => { timeLeftToComplete: null, isInActivityFlow: false, image: null, + targetSubject: null, }; return expectedItem; }; @@ -152,6 +154,7 @@ const getScheduledEventEntity = (settings: { timer: null, }, }, + targetSubject: null, }; return result; }; @@ -175,6 +178,7 @@ const getAlwaysAvailableEventEntity = (settings: { scheduledAt: Date }): EventEn timer: null, }, }, + targetSubject: null, }; return result; @@ -903,8 +907,6 @@ describe('ActivityGroupsBuilder', () => { progress, }; - let builder = createActivityGroupsBuilder(input); - const eventEntity: EventEntity = getScheduledEventEntity({ scheduledAt, startDate: subDays(startOfDay(scheduledAt), 2), @@ -929,7 +931,7 @@ describe('ActivityGroupsBuilder', () => { progress, }; - builder = createActivityGroupsBuilder(input); + const builder = createActivityGroupsBuilder(input); const now = addMinutes(scheduledAt, 10); @@ -939,18 +941,9 @@ describe('ActivityGroupsBuilder', () => { expect(result).toEqual(expectedResult); }); - it('5Should not return group-item for scheduled event and periodicity is Weekly and allowAccessBeforeFromTime is false when started yesterday, but not completed yet', () => { + it('Should not return group-item for scheduled event and periodicity is Weekly and allowAccessBeforeFromTime is false when started yesterday, but not completed yet', () => { const scheduledAt = new Date(2023, 8, 1, 15, 0, 0); - let progress: GroupProgressState = getEmptyProgress(); - - let input: GroupsBuildContext = { - allAppletActivities: [], - progress, - }; - - let builder = createActivityGroupsBuilder(input); - const eventEntity: EventEntity = getScheduledEventEntity({ scheduledAt, startDate: subDays(startOfDay(scheduledAt), 2), @@ -968,14 +961,14 @@ describe('ActivityGroupsBuilder', () => { activities: [], }; - progress = getProgress(subDays(scheduledAt, 1), null); + const progress = getProgress(subDays(scheduledAt, 1), null); - input = { + const input = { allAppletActivities: [], progress, }; - builder = createActivityGroupsBuilder(input); + const builder = createActivityGroupsBuilder(input); const now = addMinutes(scheduledAt, 10); @@ -1108,8 +1101,6 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.startDate = subMonths(now, 3); eventEntity.event.availability.endDate = subMonths(now, 2); - let result = builder.buildAvailable([eventEntity]); - const expectedItem = getExpectedAvailableItem(); expectedItem.availableTo = new Date(startOfDay(scheduledAt)); expectedItem.availableTo.setHours(16); @@ -1121,7 +1112,7 @@ describe('ActivityGroupsBuilder', () => { activities: [], }; - result = builder.buildAvailable([eventEntity]); + const result = builder.buildAvailable([eventEntity]); expect(result).toEqual(expectedEmptyResult); }); @@ -1154,8 +1145,6 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.startDate = subMonths(now, 2); eventEntity.event.availability.endDate = addMonths(now, 2); - let result = builder.buildAvailable([eventEntity]); - const expectedItem = getExpectedAvailableItem(); expectedItem.availableTo = new Date(startOfDay(scheduledAt)); expectedItem.availableTo.setHours(16); @@ -1170,7 +1159,7 @@ describe('ActivityGroupsBuilder', () => { activities: [expectedItem], }; - result = builder.buildAvailable([eventEntity]); + const result = builder.buildAvailable([eventEntity]); expect(result).toEqual(expectedResult); }); @@ -1375,8 +1364,6 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.startDate = addMonths(now, 2); eventEntity.event.availability.endDate = addMonths(now, 3); - let result = builder.buildScheduled([eventEntity]); - const expectedItem: ActivityListItem = getExpectedScheduledItem(); expectedItem.availableFrom = startOfDay(scheduledAt); expectedItem.availableFrom.setHours(15); @@ -1390,7 +1377,7 @@ describe('ActivityGroupsBuilder', () => { activities: [expectedItem], }; - result = builder.buildScheduled([eventEntity]); + const result = builder.buildScheduled([eventEntity]); expect(result).toEqual(expectedEmptyResult); }); @@ -1424,8 +1411,6 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.startDate = subMonths(now, 3); eventEntity.event.availability.endDate = subMonths(now, 2); - let result = builder.buildScheduled([eventEntity]); - const expectedItem: ActivityListItem = getExpectedScheduledItem(); expectedItem.availableFrom = startOfDay(scheduledAt); expectedItem.availableFrom.setHours(15); @@ -1439,7 +1424,7 @@ describe('ActivityGroupsBuilder', () => { activities: [expectedItem], }; - result = builder.buildScheduled([eventEntity]); + const result = builder.buildScheduled([eventEntity]); expect(result).toEqual(expectedEmptyResult); }); @@ -1471,8 +1456,6 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.timeFrom = { hours: 15, minutes: 0 }; eventEntity.event.availability.timeTo = { hours: 16, minutes: 30 }; - let result = builder.buildScheduled([eventEntity]); - const expectedItem: ActivityListItem = getExpectedScheduledItem(); expectedItem.availableFrom = startOfDay(scheduledAt); expectedItem.availableFrom.setHours(15); @@ -1500,7 +1483,7 @@ describe('ActivityGroupsBuilder', () => { eventEntity.event.availability.startDate = subMonths(now, 2); eventEntity.event.availability.endDate = addMonths(now, 2); - result = builder.buildScheduled([eventEntity]); + const result = builder.buildScheduled([eventEntity]); expect(result).toEqual(expectedResult); }); @@ -1611,6 +1594,7 @@ describe('ActivityGroupsBuilder', () => { type: ActivityType.NotDefined, order: 0, image: null, + autoAssign: true, }, { description: 'test-description-2', @@ -1621,6 +1605,7 @@ describe('ActivityGroupsBuilder', () => { type: ActivityType.NotDefined, order: 1, image: null, + autoAssign: true, }, ], progress, @@ -1638,6 +1623,7 @@ describe('ActivityGroupsBuilder', () => { isHidden: false, order: 0, image: null, + autoAssign: true, }; const eventEntity: EventEntity = { @@ -1656,6 +1642,7 @@ describe('ActivityGroupsBuilder', () => { timer: null, }, }, + targetSubject: null, }; let result = builder.buildInProgress([eventEntity]); @@ -1684,6 +1671,7 @@ describe('ActivityGroupsBuilder', () => { numberOfActivitiesInFlow: 2, activityPositionInFlow: 1, }, + targetSubject: null, }, ], name: 'additional.in_progress', @@ -1725,6 +1713,7 @@ describe('ActivityGroupsBuilder', () => { numberOfActivitiesInFlow: 2, activityPositionInFlow: 2, }, + targetSubject: null, }, ], name: 'additional.in_progress', diff --git a/src/abstract/lib/GroupBuilder/AvailableGroupBuilder.test.ts b/src/abstract/lib/GroupBuilder/AvailableGroupBuilder.test.ts index a132db70a..e38b81cf1 100644 --- a/src/abstract/lib/GroupBuilder/AvailableGroupBuilder.test.ts +++ b/src/abstract/lib/GroupBuilder/AvailableGroupBuilder.test.ts @@ -44,6 +44,7 @@ const getActivity = (): Entity => { order: 0, type: ActivityType.NotDefined, image: null, + autoAssign: true, }; return result; }; @@ -104,6 +105,7 @@ const getScheduledEventEntity = (settings: { timer: null, }, }, + targetSubject: null, }; return result; diff --git a/src/abstract/lib/GroupBuilder/ScheduledGroupEvaluator.test.ts b/src/abstract/lib/GroupBuilder/ScheduledGroupEvaluator.test.ts index 322cb17c7..9ca0fc2cf 100644 --- a/src/abstract/lib/GroupBuilder/ScheduledGroupEvaluator.test.ts +++ b/src/abstract/lib/GroupBuilder/ScheduledGroupEvaluator.test.ts @@ -43,6 +43,7 @@ const getActivity = (): Entity => { order: 0, type: ActivityType.NotDefined, image: null, + autoAssign: true, }; return result; }; @@ -103,6 +104,7 @@ const getScheduledEventEntity = (settings: { timer: null, }, }, + targetSubject: null, }; return result; From a3cf7d9e26daa72f0c3e0a9397e0a70d17b6913e Mon Sep 17 00:00:00 2001 From: farmerpaul Date: Tue, 3 Sep 2024 17:54:54 -0300 Subject: [PATCH 8/9] test: add tests for `ActivityGroupsBuildManager` Also fix tsconfig.node.json to exclude node_modules from tsc. --- src/test/utils/mock.ts | 296 ++++++++++++++++++ .../ActivityGroupsBuildManager.test.ts | 188 +++++++++++ tsconfig.node.json | 1 + 3 files changed, 485 insertions(+) create mode 100644 src/test/utils/mock.ts create mode 100644 src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.test.ts diff --git a/src/test/utils/mock.ts b/src/test/utils/mock.ts new file mode 100644 index 000000000..8becdd383 --- /dev/null +++ b/src/test/utils/mock.ts @@ -0,0 +1,296 @@ +import { ActivityPipelineType, GroupProgressId, GroupProgressState } from '~/abstract/lib'; +import { AvailabilityLabelType } from '~/entities/event'; +import { + ActivityBaseDTO, + ActivityFlowDTO, + AppletEventsResponse, + HydratedAssignmentDTO, +} from '~/shared/api'; +import { SubjectDTO } from '~/shared/api/types/subject'; + +export const mockAppletId = 'applet-1'; + +export const mockActivityId1 = 'activity-1'; +export const mockActivityId2 = 'activity-2'; +export const mockActivityId3 = 'activity-3'; +export const mockActivityId4 = 'activity-4'; + +export const mockActivities: ActivityBaseDTO[] = [ + { + id: mockActivityId1, + name: 'Test Activity', + description: '', + image: '', + isHidden: false, + order: 0, + containsResponseTypes: ['singleSelect'], + itemCount: 1, + autoAssign: true, + }, + { + id: mockActivityId2, + name: 'Hidden Activity', + description: '', + image: '', + isHidden: true, + order: 1, + containsResponseTypes: ['singleSelect'], + itemCount: 1, + autoAssign: true, + }, + { + id: mockActivityId3, + name: 'Manually Assigned Activity', + description: '', + image: '', + isHidden: false, + order: 2, + containsResponseTypes: ['singleSelect'], + itemCount: 1, + autoAssign: false, + }, + { + id: mockActivityId4, + name: 'Manually Assigned Activity 2', + description: '', + image: '', + isHidden: false, + order: 3, + containsResponseTypes: ['singleSelect'], + itemCount: 1, + autoAssign: false, + }, +]; + +export const mockFlowId1 = 'flow-1'; +export const mockFlowId2 = 'flow-2'; +export const mockFlowId3 = 'flow-3'; + +export const mockFlows: ActivityFlowDTO[] = [ + { + id: mockFlowId1, + name: 'Test Flow', + description: '', + image: '', + isSingleReport: false, + hideBadge: false, + order: 0, + isHidden: false, + activityIds: [mockActivityId1, mockActivityId2, mockActivityId3], + autoAssign: true, + }, + { + id: mockFlowId2, + name: 'Hidden Flow', + description: '', + image: '', + isSingleReport: false, + hideBadge: false, + order: 1, + isHidden: true, + activityIds: [mockActivityId1, mockActivityId2, mockActivityId3], + autoAssign: true, + }, + { + id: mockFlowId3, + name: 'Manually Assigned Flow', + description: '', + image: '', + isSingleReport: false, + hideBadge: false, + order: 2, + isHidden: false, + activityIds: [mockActivityId1, mockActivityId2, mockActivityId3], + autoAssign: false, + }, +]; + +export const mockUserId1 = 'user-1'; + +export const mockSubjectId1 = 'subject-1'; +export const mockSubjectId2 = 'subject-2'; + +export const mockSubject1: SubjectDTO = { + id: mockSubjectId1, + userId: mockUserId1, + firstName: 'Full', + lastName: 'Account', + secretUserId: 'subject-secret-1', + appletId: mockAppletId, + nickname: 'Full Account', + tag: null, + lastSeen: null, +}; +export const mockSubject2: SubjectDTO = { + id: mockSubjectId2, + userId: null, + firstName: 'Limited', + lastName: 'Account', + secretUserId: 'subject-secret-2', + appletId: mockAppletId, + nickname: 'Limited Account', + tag: null, + lastSeen: null, +}; + +export const mockAssignments: HydratedAssignmentDTO[] = [ + { + activityId: mockActivityId1, + activityFlowId: null, + respondentSubject: mockSubject1, + targetSubject: mockSubject1, + }, + { + activityId: mockActivityId3, + activityFlowId: null, + respondentSubject: mockSubject1, + targetSubject: mockSubject1, + }, + { + activityId: mockActivityId3, + activityFlowId: null, + respondentSubject: mockSubject1, + targetSubject: mockSubject2, + }, + { + activityId: null, + activityFlowId: mockFlowId3, + respondentSubject: mockSubject1, + targetSubject: mockSubject1, + }, + { + activityId: null, + activityFlowId: mockFlowId3, + respondentSubject: mockSubject1, + targetSubject: mockSubject2, + }, +]; + +const mockEventId1 = 'event-1'; +const mockEventId2 = 'event-2'; +const mockEventId3 = 'event-3'; +const mockEventId4 = 'event-4'; +const mockEventId5 = 'event-5'; +const mockEventId6 = 'event-6'; + +export const mockEventsResponse: AppletEventsResponse = { + events: [ + { + id: mockEventId1, + entityId: mockActivityId1, + availabilityType: AvailabilityLabelType.AlwaysAvailable, + availability: { + oneTimeCompletion: false, + periodicityType: 'ALWAYS', + timeFrom: { hours: 0, minutes: 0 }, + timeTo: { hours: 0, minutes: 0 }, + allowAccessBeforeFromTime: false, + }, + selectedDate: null, + timers: { + timer: null, + idleTimer: null, + }, + }, + { + id: mockEventId2, + entityId: mockActivityId2, + availabilityType: AvailabilityLabelType.AlwaysAvailable, + availability: { + oneTimeCompletion: false, + periodicityType: 'ALWAYS', + timeFrom: { hours: 0, minutes: 0 }, + timeTo: { hours: 0, minutes: 0 }, + allowAccessBeforeFromTime: false, + }, + selectedDate: null, + timers: { + timer: null, + idleTimer: null, + }, + }, + { + id: mockEventId3, + entityId: mockActivityId3, + availabilityType: AvailabilityLabelType.ScheduledAccess, + availability: { + oneTimeCompletion: false, + periodicityType: 'DAILY', + timeFrom: { hours: 20, minutes: 0 }, + timeTo: { hours: 24, minutes: 0 }, + allowAccessBeforeFromTime: false, + }, + selectedDate: null, + timers: { + timer: null, + idleTimer: null, + }, + }, + { + id: mockEventId4, + entityId: mockFlowId1, + availabilityType: AvailabilityLabelType.AlwaysAvailable, + availability: { + oneTimeCompletion: false, + periodicityType: 'ALWAYS', + timeFrom: { hours: 0, minutes: 0 }, + timeTo: { hours: 0, minutes: 0 }, + allowAccessBeforeFromTime: false, + }, + selectedDate: null, + timers: { + timer: null, + idleTimer: null, + }, + }, + { + id: mockEventId5, + entityId: mockFlowId2, + availabilityType: AvailabilityLabelType.AlwaysAvailable, + availability: { + oneTimeCompletion: false, + periodicityType: 'ALWAYS', + timeFrom: { hours: 0, minutes: 0 }, + timeTo: { hours: 0, minutes: 0 }, + allowAccessBeforeFromTime: false, + }, + selectedDate: null, + timers: { + timer: null, + idleTimer: null, + }, + }, + { + id: mockEventId6, + entityId: mockFlowId3, + availabilityType: AvailabilityLabelType.AlwaysAvailable, + availability: { + oneTimeCompletion: false, + periodicityType: 'ALWAYS', + timeFrom: { hours: 0, minutes: 0 }, + timeTo: { hours: 0, minutes: 0 }, + allowAccessBeforeFromTime: false, + }, + selectedDate: null, + timers: { + timer: null, + idleTimer: null, + }, + }, + ], +}; + +export const mockEntityProgressId: GroupProgressId = `${mockFlowId3}/${mockEventId6}`; + +export const mockEntityProgress: GroupProgressState = { + [mockEntityProgressId]: { + type: ActivityPipelineType.Flow, + currentActivityId: mockActivityId1, + pipelineActivityOrder: 1, + currentActivityStartAt: null, + executionGroupKey: 'test-key', + context: { summaryData: {} }, + startAt: 1, + endAt: null, + }, +}; diff --git a/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.test.ts b/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.test.ts new file mode 100644 index 000000000..2ccc3eabd --- /dev/null +++ b/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.test.ts @@ -0,0 +1,188 @@ +import { ActivityGroupsBuildManager } from './ActivityGroupsBuildManager'; + +import { ActivityGroupType } from '~/abstract/lib/GroupBuilder'; +import { + mockActivities, + mockActivityId1, + mockActivityId3, + mockAssignments, + mockEntityProgress, + mockEventsResponse, + mockFlowId1, + mockFlowId3, + mockFlows, + mockSubject2, +} from '~/test/utils/mock'; + +jest.setSystemTime(new Date('2024-09-01T12:00:00.000Z')); + +describe('ActivityGroupsBuildManager', () => { + it('should build activity groups correctly with assignments disabled', () => { + const { groups } = ActivityGroupsBuildManager.process({ + activities: mockActivities, + flows: mockFlows, + assignments: null, + events: mockEventsResponse, + entityProgress: {}, + }); + + expect(groups).toMatchObject([ + { + activities: [], + type: ActivityGroupType.InProgress, + }, + { + activities: [ + { flowId: mockFlowId1 }, + { flowId: mockFlowId3 }, + { activityId: mockActivityId1 }, + ], + type: ActivityGroupType.Available, + }, + { + activities: [{ activityId: mockActivityId3 }], + type: ActivityGroupType.Scheduled, + }, + ]); + }); + + it('should build activity groups correctly with assignments disabled and in-progress flow', () => { + const { groups } = ActivityGroupsBuildManager.process({ + activities: mockActivities, + flows: mockFlows, + assignments: null, + events: mockEventsResponse, + entityProgress: mockEntityProgress, + }); + + expect(groups).toMatchObject([ + { + activities: [{ flowId: mockFlowId3 }], + type: ActivityGroupType.InProgress, + }, + { + activities: [{ flowId: mockFlowId1 }, { activityId: mockActivityId1 }], + type: ActivityGroupType.Available, + }, + { + activities: [{ activityId: mockActivityId3 }], + type: ActivityGroupType.Scheduled, + }, + ]); + }); + + it('should build activity groups correctly with mock assignments', () => { + const { groups } = ActivityGroupsBuildManager.process({ + activities: mockActivities, + flows: mockFlows, + assignments: mockAssignments, + events: mockEventsResponse, + entityProgress: {}, + }); + + expect(groups).toMatchObject([ + { + activities: [], + type: ActivityGroupType.InProgress, + }, + { + activities: [ + { flowId: mockFlowId1 }, + { flowId: mockFlowId3 }, + { flowId: mockFlowId3, targetSubject: mockSubject2 }, + { activityId: mockActivityId1 }, + ], + type: ActivityGroupType.Available, + }, + { + activities: [ + { activityId: mockActivityId3 }, + { activityId: mockActivityId3, targetSubject: mockSubject2 }, + ], + type: ActivityGroupType.Scheduled, + }, + ]); + }); + + it('should build activity groups correctly with mock assignments and in-progress flow', () => { + const { groups } = ActivityGroupsBuildManager.process({ + activities: mockActivities, + flows: mockFlows, + assignments: mockAssignments, + events: mockEventsResponse, + entityProgress: mockEntityProgress, + }); + + expect(groups).toMatchObject([ + { + activities: [{ flowId: mockFlowId3 }], + type: ActivityGroupType.InProgress, + }, + { + activities: [ + { flowId: mockFlowId1 }, + { flowId: mockFlowId3, targetSubject: mockSubject2 }, + { activityId: mockActivityId1 }, + ], + type: ActivityGroupType.Available, + }, + { + activities: [ + { activityId: mockActivityId3 }, + { activityId: mockActivityId3, targetSubject: mockSubject2 }, + ], + type: ActivityGroupType.Scheduled, + }, + ]); + }); + + it('should handle empty activities and flows', () => { + const { groups } = ActivityGroupsBuildManager.process({ + activities: [], + flows: [], + assignments: null, + events: { events: [] }, + entityProgress: {}, + }); + + expect(groups).toMatchObject([ + { + activities: [], + type: ActivityGroupType.InProgress, + }, + { + activities: [], + type: ActivityGroupType.Available, + }, + { + activities: [], + type: ActivityGroupType.Scheduled, + }, + ]); + }); + + it('should handle empty events', () => { + const { groups } = ActivityGroupsBuildManager.process({ + activities: mockActivities, + flows: mockFlows, + assignments: mockAssignments, + events: { events: [] }, + entityProgress: {}, + }); + + expect(groups).toMatchObject([ + { + activities: [], + type: ActivityGroupType.InProgress, + }, + { + activities: [], + type: ActivityGroupType.Available, + }, + { + activities: [], + type: ActivityGroupType.Scheduled, + }, + ]); + }); +}); diff --git a/tsconfig.node.json b/tsconfig.node.json index cc691010b..b41ba77eb 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -4,6 +4,7 @@ "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true, + "skipLibCheck": true }, "include": ["vite.config.mts"] } From d9a66a8d3fdce6f8010a1c91a161d8657fcdb544 Mon Sep 17 00:00:00 2001 From: farmerpaul Date: Wed, 4 Sep 2024 16:48:26 -0300 Subject: [PATCH 9/9] fix: fixes for @mbanting PR comments --- .../PassSurvey/hooks/useSurveyDataQuery.ts | 28 ++++++++++++------- src/i18n/en/translation.json | 8 +++--- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/features/PassSurvey/hooks/useSurveyDataQuery.ts b/src/features/PassSurvey/hooks/useSurveyDataQuery.ts index 8b11e7c29..325deff4f 100644 --- a/src/features/PassSurvey/hooks/useSurveyDataQuery.ts +++ b/src/features/PassSurvey/hooks/useSurveyDataQuery.ts @@ -2,8 +2,8 @@ import { UseQueryResult } from '@tanstack/react-query'; import { useActivityByIdQuery } from '~/entities/activity'; import { useAppletByIdQuery } from '~/entities/applet'; +import { useMyAssignmentsQuery } from '~/entities/assignment'; import { useEventsbyAppletIdQuery } from '~/entities/event'; -import { useSubjectQuery } from '~/entities/subject'; import { ActivityDTO, AppletDTO, @@ -35,7 +35,8 @@ type Props = { export const useSurveyDataQuery = (props: Props): Return => { const { appletId, activityId, publicAppletKey, targetSubjectId } = props; const { featureFlags } = useFeatureFlags(); - const isAssignmentsEnabled = !!featureFlags?.enableActivityAssign && !!targetSubjectId; + const isAssignmentsEnabled = + !!featureFlags?.enableActivityAssign && !!appletId && !!targetSubjectId; const { data: appletById, @@ -44,7 +45,7 @@ export const useSurveyDataQuery = (props: Props): Return => { error: appletError, } = useAppletByIdQuery( publicAppletKey ? { isPublic: true, publicAppletKey } : { isPublic: false, appletId }, - { select: (data) => data?.data }, + { select: ({ data }) => data }, ); const { @@ -54,7 +55,7 @@ export const useSurveyDataQuery = (props: Props): Return => { error: activityError, } = useActivityByIdQuery( { isPublic: !!publicAppletKey, activityId }, - { select: (data) => data?.data?.result }, + { select: ({ data }) => data.result }, ); const { @@ -64,20 +65,27 @@ export const useSurveyDataQuery = (props: Props): Return => { error: eventsError, } = useEventsbyAppletIdQuery( publicAppletKey ? { isPublic: true, publicAppletKey } : { isPublic: false, appletId }, - { select: (data) => data?.data?.result }, + { select: ({ data }) => data.result }, ); - const subjectQueryResult = useSubjectQuery(targetSubjectId, { - select: (data) => data.data.result, - enabled: isAssignmentsEnabled, - }); + // Details of targetSubject are only guaranteed available from /users/me/assignments endpoint + // (Unprivileged users do not have access to the /subjects endpoint directly) + const assignmentsResult = useMyAssignmentsQuery( + isAssignmentsEnabled ? props.appletId : undefined, + { + select: ({ data }) => + data.result.assignments.find(({ targetSubject: { id } }) => id === targetSubjectId) + ?.targetSubject, + enabled: isAssignmentsEnabled, + }, + ); const { data: targetSubject, isError: isSubjectError, isLoading: isSubjectLoading, error: subjectError, - } = isAssignmentsEnabled ? subjectQueryResult : ({} as UseQueryResult); + } = isAssignmentsEnabled ? assignmentsResult : ({} as UseQueryResult); return { appletDTO: appletById?.result ?? null, diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json index 85f5bd6a4..59825675d 100644 --- a/src/i18n/en/translation.json +++ b/src/i18n/en/translation.json @@ -19,15 +19,15 @@ "pleaseProvideAdditionalText": "Please provide additional text", "pleaseListenToAudio": "Please listen to the audio until the end.", "onlyNumbersAllowed": "Only numbers are allowed", - "questionCount_one": "{{count}} question", - "questionCount_other": "{{count}} questions", + "questionCount_one": "{{count}} Question", + "questionCount_other": "{{count}} Questions", "timedActivityTitle": "is a Timed Activity.", "youWillHaveToCompleteIt": "You will have {{hours}} hours {{minutes}} minutes to complete it.", "yourWorkWillBeSubmitted": "Your work will be auto-submitted when time runs out.", "countOfCompletedQuestions_one": "{{countOfCompletedQuestions}} of {{count}} question completed", "countOfCompletedQuestions_other": "{{countOfCompletedQuestions}} of {{count}} questions completed", - "activityFlowLength_one": "{{count}} activity", - "activityFlowLength_other": "{{count}} activities", + "activityFlowLength_one": "{{count}} Activity", + "activityFlowLength_other": "{{count}} Activities", "countOfCompletedActivities_one": "{{countOfCompletedActivities}} of {{count}} activity completed", "countOfCompletedActivities_other": "{{countOfCompletedActivities}} of {{count}} activities completed", "pleaseCompleteOnThe": "Please complete on the",