Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feature: Constrain Take Now to availability schedule (M2-7180) #1868

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
175 changes: 172 additions & 3 deletions src/modules/Dashboard/components/ActivityGrid/ActivityGrid.utils.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
import { t } from 'i18next';
import {
isWithinInterval,
getDay,
getDate,
getYear,
getMonth,
getHours,
getMinutes,
getSeconds,
format,
isWeekend,
isAfter,
setDate,
setMonth,
setYear,
} from 'date-fns';

import { Svg } from 'shared/components/Svg';
import { MenuItem, MenuItemType } from 'shared/components';
Expand All @@ -10,9 +26,160 @@ import {
getIsWebSupported,
} from 'shared/utils';
import { EditablePerformanceTasks } from 'modules/Builder/features/Activities/Activities.const';
import { PeriodicityType } from 'shared/state/Applet/Applet.schema';

import { ActivityActions, ActivityActionProps } from './ActivityGrid.types';

/**
* this function checks if the current date is within the interval passed as arguments
* @param start Date
* @param end Date
* @returns boolean
*/
function datetimeIsWithinInterval(start: Date, end: Date) {
const today = new Date();
if (isAfter(start, end)) {
console.error('The start date is after the end date, be sure to check the dates');

// using the isWithinInterval function with the start and end dates swapped
// in case the start date is after the end date
return isWithinInterval(today, { start: end, end: start });
}

return isWithinInterval(today, { start, end });
}

/**
* this function returns the values of the date represented as an object
* @param date Date
* @returns object
*/
function getDateValues(date: Date) {
if (!date || !(date instanceof Date)) {
console.error('The date is not valid, be sure to pass a valid date');

return {
day: 0,
month: 0,
year: 0,
hour: 0,
minute: 0,
second: 0,
dayInWeek: 0,
date: new Date(),
};
}

return {
day: getDate(date),
month: getMonth(date) + 1,
year: getYear(date),
hour: getHours(date),
minute: getMinutes(date),
second: getSeconds(date),
dayInWeek: getDay(date),
date,
};
}

const PERIODICITY_VALUES = {
ONCE: 'ONCE',
ALWAYS: 'ALWAYS',
DAILY: 'DAILY',
WEEKLY: 'WEEKLY',
WEEKDAYS: 'WEEKDAYS',
MONTHLY: 'MONTHLY',
};

/**
* this function formats the date as YYYY-MM-DD
* @param date Date
* @returns string
*/
function formatDateAsYYYYMMDD(date: Date) {
if (!date || !(date instanceof Date)) {
console.error('The date is not valid, be sure to pass a valid date');
}

return format(date, 'yyyy-MM-dd');
}

/**
* this function validates if the date is in range based on the PeriodicityTypes object
* the options of validation are:
* - if the periodicity is always, which means that the activity is always available
* - if the periodicity is once, which means that the activity is available only once
* - if the periodicity is daily, which means that the activity is available every day
* - if the periodicity is weekly, which means that the activity is available every week at the same week day
* - if the periodicity is weekdays, which means that the activity is available every weekday and not in weekends
* - if the periodicity is monthly, which means that the activity is available every month at the same day
* - if the periodicity is not set
* @param periodicity PeriodicityType
* @returns boolean
*/
function validateIfDateIsInRange(periodicity: PeriodicityType) {
if (!periodicity) {
console.error('The periodicity is not set, be sure to pass a valid periodicity');

return false;
}

if (periodicity.access_before_schedule || periodicity.type === PERIODICITY_VALUES.ALWAYS) {
return true;
}

const currentDate = new Date();
const currentTimeValues = getDateValues(currentDate);
const startDate = new Date(
`${periodicity.start_date ? periodicity.start_date : formatDateAsYYYYMMDD(currentDate)}T${
periodicity.start_time
}`,
);
const endDate = new Date(
`${periodicity?.end_date ? periodicity.end_date : formatDateAsYYYYMMDD(currentDate)}T${
periodicity.end_time
}`,
);

const startTimeValues = getDateValues(startDate);

if (
periodicity.type === PERIODICITY_VALUES.ONCE ||
periodicity.type === PERIODICITY_VALUES.DAILY
) {
const todayEndDateWithEndTime = setYear(
setMonth(setDate(endDate, currentTimeValues.day), currentTimeValues.month - 1),
currentTimeValues.year,
);

return datetimeIsWithinInterval(startDate, todayEndDateWithEndTime);
} else if (periodicity.type === PERIODICITY_VALUES.WEEKLY) {
if (currentTimeValues.dayInWeek !== startTimeValues.dayInWeek) {
return false;
}

return datetimeIsWithinInterval(startDate, endDate);
} else if (periodicity.type === PERIODICITY_VALUES.WEEKDAYS) {
if (isWeekend(currentDate)) {
return false;
}

const todayEndDateWithEndTime = setYear(
setMonth(setDate(endDate, currentTimeValues.day), currentTimeValues.month - 1),
currentTimeValues.year,
);

return datetimeIsWithinInterval(startDate, todayEndDateWithEndTime);
} else if (
periodicity.type === PERIODICITY_VALUES.MONTHLY &&
currentTimeValues.day === startTimeValues.day
) {
return datetimeIsWithinInterval(startDate, endDate);
}

return false;
}

export const getActivityActions = ({
actions: { editActivity, exportData, assignActivity, takeNow },
appletId,
Expand All @@ -34,7 +201,9 @@ export const getActivityActions = ({
const { id: activityId } = activity;
const isWebUnsupported = !getIsWebSupported(activity.items);

if (!activityId) return [];
if (!activityId || !activity?.periodicity) return [];

const isInRange = validateIfDateIsInRange(activity?.periodicity);

return [
{
Expand Down Expand Up @@ -68,8 +237,8 @@ export const getActivityActions = ({
title: t('takeNow.menuItem'),
context: { appletId, activityId },
isDisplayed: canDoTakeNow,
disabled: isWebUnsupported,
tooltip: isWebUnsupported && t('activityIsMobileOnly'),
disabled: isWebUnsupported || !isInRange,
tooltip: (!isInRange && t('')) || (isWebUnsupported && t('activityIsMobileOnly')),
'data-testid': `${dataTestId}-activity-take-now`,
},
];
Expand Down
82 changes: 56 additions & 26 deletions src/shared/state/Applet/Applet.schema.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { ColorResult } from 'react-color';

import { BaseSchema } from 'shared/state/Base';
import { ElementType, RetentionPeriods } from 'shared/types';
import {
ItemResponseType,
SubscaleTotalScore,
CorrectPress,
DeviceType,
FlankerSamplingMethod,
OrderName,
RoundTypeEnum,
} from 'modules/Builder/types';
import {
CalculationType,
ConditionType,
ScoreConditionType,
ConditionalLogicMatch,
CalculationType,
PerfTaskType,
GyroscopeOrTouch,
ItemResponseType,
PerfTaskType,
ScoreConditionType,
ScoreReportType,
SubscaleTotalScore,
} from 'shared/consts';
import { BaseSchema } from 'shared/state/Base';
import { ElementType, RetentionPeriods } from 'shared/types';
import { Encryption } from 'shared/utils/encryption';
import {
CorrectPress,
RoundTypeEnum,
FlankerSamplingMethod,
DeviceType,
OrderName,
} from 'modules/Builder/types';

type ActivityFlowItem = {
activityId: string;
Expand Down Expand Up @@ -661,26 +661,56 @@ export type ScoresObject = {
[key: string]: number;
};

// types from the BE repo defined in src/apps/schedule/domain/constants.py with the same name as the following:
export type PeriodicityTypeValues = {
ONCE: 'ONCE';
DAILY: 'DAILY';
WEEKLY: 'WEEKLY';
WEEKDAYS: 'WEEKDAYS';
MONTHLY: 'MONTHLY';
ALWAYS: 'ALWAYS';
};

export type TimerType = {
NOT_SET: 'NOT_SET';
TIMER: 'TIMER';
IDLE: 'IDLE';
};

export type PeriodicityType = {
access_before_schedule: boolean;
end_date: string; // Format: "YYYY-MM-DD"
end_time: string; // Format: "HH:MM:SS"
one_time_completion: null | string;
selected_date: null | string;
start_date: string; // Format: "YYYY-MM-DD"
start_time: string; // Format: "HH:MM:SS"
timer: null | number;
timer_type: TimerType[keyof TimerType];
type: PeriodicityTypeValues[keyof PeriodicityTypeValues];
};

export type Activity = {
createdAt?: string;
description: string | Record<string, string>;
id?: string;
image?: string;
isHidden?: boolean;
isPerformanceTask?: boolean;
isReviewable?: boolean;
isSkippable?: boolean;
items: Item[];
key?: string;
name: string;
order?: number;
description: string | Record<string, string>;
splashScreen?: string;
image?: string;
showAllAtOnce?: boolean;
isSkippable?: boolean;
isReviewable?: boolean;
performanceTaskType?: PerfTaskType | null;
periodicity?: PeriodicityType;
reportIncludedItemName?: string;
responseIsEditable?: boolean;
isHidden?: boolean;
items: Item[];
scoresAndReports?: ScoresAndReports;
showAllAtOnce?: boolean;
splashScreen?: string;
subscaleSetting?: SubscaleSetting | null;
isPerformanceTask?: boolean;
performanceTaskType?: PerfTaskType | null;
createdAt?: string;
reportIncludedItemName?: string;
};

type Theme = {
Expand Down
Loading